blob: d5fe450f3a6f8945e97c7223e3bbd241bb68507a [file] [log] [blame]
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Tony Mantler581edd42014-02-18 15:41:22 -080019import android.annotation.SuppressLint;
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -070020import android.annotation.TargetApi;
Mindy Pereira326c6602012-01-04 15:32:42 -080021import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070022import android.app.ActionBar.OnNavigationListener;
23import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080024import android.app.ActivityManager;
25import android.app.AlertDialog;
26import android.app.Dialog;
Tony Mantler2558b502013-07-09 10:53:34 -070027import android.app.DialogFragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070028import android.app.Fragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070029import android.app.FragmentTransaction;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070030import android.app.LoaderManager;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070031import android.content.ClipData;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080033import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080034import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070035import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080036import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080037import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070038import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.content.pm.ActivityInfo;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -070040import android.content.res.Resources;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080041import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080042import android.net.Uri;
Alan Lau15490232014-03-06 14:53:14 -080043import android.os.AsyncTask;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070044import android.os.Build;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080045import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080046import android.os.Handler;
47import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070048import android.os.ParcelFileDescriptor;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.provider.BaseColumns;
Alan Lau439aa5d2014-05-27 17:57:13 -070050import android.support.v4.app.RemoteInput;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080051import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080052import android.text.Html;
mindyped9c2f02012-10-12 10:02:08 -070053import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080054import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080055import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080056import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080057import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080058import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070059import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070060import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080061import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080062import android.view.Menu;
63import android.view.MenuInflater;
64import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080065import android.view.View;
66import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070067import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080068import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070069import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080070import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080071import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070072import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080073import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080074import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080075
Mindy Pereirac17d0732011-12-29 10:46:19 -080076import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070077import com.android.common.contacts.DataUsageStatUpdater;
Tony Mantler821e5782014-01-06 15:33:43 -080078import com.android.emailcommon.mail.Address;
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -070079import com.android.ex.chips.BaseRecipientAdapter;
80import com.android.ex.chips.DropdownChipLayouter;
Andy Huang5c5fd572012-04-08 18:19:29 -070081import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080082import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070083import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070084import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070085import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070086import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080087import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080088import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080089import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080090import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080091import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080092import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070093import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080094import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080095import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070096import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080097import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080098import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070099import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -0700100import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -0700101import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700102import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -0700103import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -0700104import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -0700105import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800106import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -0700107import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800108import com.android.mail.utils.LogUtils;
Alan Lau15490232014-03-06 14:53:14 -0800109import com.android.mail.utils.NotificationActionUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800110import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800111import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800112import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800113import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800114
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700115import java.io.FileNotFoundException;
116import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700117import java.io.UnsupportedEncodingException;
118import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800119import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700120import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800121import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800122import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800123import java.util.HashSet;
124import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700125import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700126import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800127import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800128
129public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700130 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700131 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
Andrew Sappersteinffd61552014-05-14 15:04:23 -0700132 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
133 RecipientEditTextView.RecipientEntryItemClickedListener {
Scott Kennedya0287a82014-04-07 14:30:13 -0700134 /**
135 * An {@link Intent} action that launches {@link ComposeActivity}, but is handled as if the
136 * {@link Activity} were launched with no special action.
137 */
138 private static final String ACTION_LAUNCH_COMPOSE =
139 "com.android.mail.intent.action.LAUNCH_COMPOSE";
140
Mindy Pereira6349a042012-01-04 11:25:01 -0800141 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700142 public static final int COMPOSE = -1;
143 public static final int REPLY = 0;
144 public static final int REPLY_ALL = 1;
145 public static final int FORWARD = 2;
146 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800147
148 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700149 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800150
Mindy Pereira326689d2012-05-17 10:14:14 -0700151 private static final String EXTRA_SHOW_CC = "showCc";
152 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800153 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800154 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700155
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700156 private static final String UTF8_ENCODING_NAME = "UTF-8";
157
158 private static final String MAIL_TO = "mailto";
159
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700160 private static final String EXTRA_SUBJECT = "subject";
161
162 private static final String EXTRA_BODY = "body";
163
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700164 /**
165 * Expected to be html formatted text.
166 */
167 private static final String EXTRA_QUOTED_TEXT = "quotedText";
168
mindypd27b6ea2012-10-05 09:43:49 -0700169 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700170
Mark Wei62066e42012-09-13 12:07:02 -0700171 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
172
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700173 // Extra that we can get passed from other activities
Tony Mantler184ec732013-10-24 13:13:49 -0700174 @VisibleForTesting
175 protected static final String EXTRA_TO = "to";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700176 private static final String EXTRA_CC = "cc";
177 private static final String EXTRA_BCC = "bcc";
178
Scott Kennedy60847252013-08-15 15:55:42 -0700179 /**
180 * An optional extra containing a {@link ContentValues} of values to be added to
181 * {@link SendOrSaveMessage#mValues}.
182 */
183 public static final String EXTRA_VALUES = "extra-values";
184
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700185 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700186 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
187 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700188
Alan Lau439aa5d2014-05-27 17:57:13 -0700189 private static final String LEGACY_WEAR_EXTRA = "com.google.android.wearable.extras";
190
Andrew Sapperstein09da9422014-05-30 09:48:08 -0700191 /**
192 * Constant value for the threshold to use for auto-complete suggestions
193 * for the to/cc/bcc fields.
194 */
195 private static final int COMPLETION_THRESHOLD = 1;
196
Mindy Pereira82cc5662012-01-09 17:29:30 -0800197 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
198 // Map containing information about requests to create new messages, and the id of the
199 // messages that were the result of those requests.
200 //
201 // This map is used when the activity that initiated the save a of a new message, is killed
202 // before the save has completed (and when we know the id of the newly created message). When
203 // a save is completed, the service that is running in the background, will update the map
204 //
205 // When a new ComposeActivity instance is created, it will attempt to use the information in
206 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
207 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
208 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700209 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800210 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800211 /**
212 * Notifies the {@code Activity} that the caller is an Email
213 * {@code Activity}, so that the back behavior may be modified accordingly.
214 *
215 * @see #onAppUpPressed
216 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700217 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800218
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700219 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700220
Scott Kennedy5680ec22013-01-07 13:15:20 -0800221 /** If set, we will clear notifications for this folder. */
222 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
223
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800224 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700225 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700226 // If this is a reply/forward then this extra will hold a uri we must query
227 // to get the original message.
228 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700229 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700230 // draft message
231 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800232 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700233 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800234 // Request numbers for activities we start
235 private static final int RESULT_PICK_ATTACHMENT = 1;
236 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700237 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700238 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700239
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700240 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
241 private static final String EXTRA_REQUEST_ID = "requestId";
242 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700243 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700244 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700245 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700246 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700247 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700248 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700249 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700250 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700251 private static final String MIME_TYPE_PHOTO = "image/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800252
Andy Huang9f855d62013-05-30 17:15:03 -0700253 private static final String KEY_INNER_SAVED_STATE = "compose_state";
254
Mindy Pereira82cc5662012-01-09 17:29:30 -0800255 /**
256 * A single thread for running tasks in the background.
257 */
Jin Cao5134be52014-05-06 19:18:38 -0700258 private final static Handler SEND_SAVE_TASK_HANDLER;
259 static {
260 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
261 handlerThread.start();
262
263 SEND_SAVE_TASK_HANDLER = new Handler(handlerThread.getLooper());
264 }
265
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700266 private boolean mUseNewChips = false;
267
Mindy Pereirac17d0732011-12-29 10:46:19 -0800268 private RecipientEditTextView mTo;
269 private RecipientEditTextView mCc;
270 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800271 private Button mCcBccButton;
272 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800273 private AttachmentsView mAttachmentsView;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700274 protected Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700275 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800276 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800277 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800278 private TextView mSubject;
279
Mindy Pereira326c6602012-01-04 15:32:42 -0800280 private ComposeModeAdapter mComposeModeAdapter;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700281 protected int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800282 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800283 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700284 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800285 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800286 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800287 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700288 @VisibleForTesting
289 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800290 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800291 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800292 private boolean mTextChanged;
293 private boolean mReplyFromChanged;
294 private MenuItem mSave;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700295 @VisibleForTesting
296 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800297 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
298 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800299 private ReplyFromAccount mDraftAccount;
Tony Mantler581edd42014-02-18 15:41:22 -0800300 private final Object mDraftLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800301
Mindy Pereira326c6602012-01-04 15:32:42 -0800302 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700303 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
304 */
305 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700306 private RecipientTextWatcher mToListener;
307 private RecipientTextWatcher mCcListener;
308 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700309 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700310 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700311 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700312 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700313
mindyp1623f9b2012-11-21 12:41:16 -0800314 // Array of the outstanding send or save tasks. Access is synchronized
315 // with the object itself
316 /* package for testing */
317 @VisibleForTesting
Tony Mantler581edd42014-02-18 15:41:22 -0800318 public final ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
mindyp1623f9b2012-11-21 12:41:16 -0800319 // FIXME: this variable is never read. related to sRequestMessageIdMap.
320 private int mRequestId;
321 private String mSignature;
322 private Account[] mAccounts;
323 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700324 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800325
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700326 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800327 * Can be called from a non-UI thread.
328 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800329 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700330 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
331 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800332 }
333
Mindy Pereira6349a042012-01-04 11:25:01 -0800334 /**
335 * Can be called from a non-UI thread.
336 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800337 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700338 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800339 }
340
341 /**
342 * Can be called from a non-UI thread.
343 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700344 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700345 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
346 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700347 }
348
349 /**
350 * Can be called from a non-UI thread.
351 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700352 public static void composeWithExtraValues(Context launcher, Account account,
353 String subject, final ContentValues extraValues) {
354 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
355 }
356
357 /**
358 * Can be called from a non-UI thread.
359 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800360 public static Intent createReplyIntent(final Context launcher, final Account account,
361 final Uri messageUri, final boolean isReplyAll) {
362 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
363 }
364
365 /**
366 * Can be called from a non-UI thread.
367 */
368 public static Intent createForwardIntent(final Context launcher, final Account account,
369 final Uri messageUri) {
370 return createActionIntent(launcher, account, messageUri, FORWARD);
371 }
372
Scott Kennedya0287a82014-04-07 14:30:13 -0700373 private static Intent createActionIntent(final Context context, final Account account,
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800374 final Uri messageUri, final int action) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700375 final Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
376 intent.setPackage(context.getPackageName());
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800377
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700378 updateActionIntent(account, messageUri, action, intent);
379
380 return intent;
381 }
382
383 @VisibleForTesting
384 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800385 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
386 intent.putExtra(EXTRA_ACTION, action);
387 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
388 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
389
390 return intent;
391 }
392
393 /**
394 * Can be called from a non-UI thread.
395 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800396 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700397 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800398 }
399
400 /**
401 * Can be called from a non-UI thread.
402 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800403 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700404 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
405 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800406 }
407
408 /**
409 * Can be called from a non-UI thread.
410 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800411 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700412 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800413 }
414
Alice Yang1ebc2db2013-03-14 21:21:44 -0700415 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
416 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700417 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700418 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700419 }
420
Scott Kennedya0287a82014-04-07 14:30:13 -0700421 private static void launch(Context context, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700422 String toAddress, String body, String quotedText, String subject,
423 final ContentValues extraValues) {
Scott Kennedya0287a82014-04-07 14:30:13 -0700424 Intent intent = new Intent(ACTION_LAUNCH_COMPOSE);
425 intent.setPackage(context.getPackageName());
Mindy Pereira6349a042012-01-04 11:25:01 -0800426 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
427 intent.putExtra(EXTRA_ACTION, action);
428 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700429 if (action == EDIT_DRAFT) {
430 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
431 } else {
432 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
433 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700434 if (toAddress != null) {
435 intent.putExtra(EXTRA_TO, toAddress);
436 }
437 if (body != null) {
438 intent.putExtra(EXTRA_BODY, body);
439 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700440 if (quotedText != null) {
441 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
442 }
443 if (subject != null) {
444 intent.putExtra(EXTRA_SUBJECT, subject);
445 }
Scott Kennedy60847252013-08-15 15:55:42 -0700446 if (extraValues != null) {
447 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
448 intent.putExtra(EXTRA_VALUES, extraValues);
449 }
Scott Kennedya0287a82014-04-07 14:30:13 -0700450 context.startActivity(intent);
Mindy Pereira6349a042012-01-04 11:25:01 -0800451 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800452
Scott Kennedya0287a82014-04-07 14:30:13 -0700453 public static void composeMailto(Context context, Account account, Uri mailto) {
454 final Intent intent = new Intent(Intent.ACTION_VIEW, mailto);
455 intent.setPackage(context.getPackageName());
Andy Huang0a2a3462013-12-20 15:56:13 -0800456 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
457 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Scott Kennedya0287a82014-04-07 14:30:13 -0700458 context.startActivity(intent);
Andy Huang0a2a3462013-12-20 15:56:13 -0800459 }
460
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800461 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700462 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800463 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800464 setContentView(R.layout.compose);
Andy Huang9f855d62013-05-30 17:15:03 -0700465 mInnerSavedState = (savedInstanceState != null) ?
466 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700467 checkValidAccounts();
468 }
469
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700470 private boolean shouldUseNewChips() {
471 // Get the Android ID from this device
472 String androidId = android.provider.Settings.Secure
473 .getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
474
475 // If we don't have a valid android id, just use account name hash code
476 if (TextUtils.isEmpty(androidId)) {
477 LogUtils.d(LOG_TAG, "Fallback to email address");
478 androidId = mAccount.getEmailAddress();
479 }
480
481 // randomly cut our userbase in half
482 return (androidId.hashCode() % 2) == 1;
483 }
484
Mindy Pereirab199d172012-08-13 11:04:03 -0700485 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700486 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800487 findViews();
Tony Mantler581edd42014-02-18 15:41:22 -0800488 final Intent intent = getIntent();
489 final Message message;
490 final ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700491 mShowQuotedText = false;
Tony Mantler581edd42014-02-18 15:41:22 -0800492 final CharSequence quotedText;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700493 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700494 // Check for any of the possibly supplied accounts.;
Tony Mantler581edd42014-02-18 15:41:22 -0800495 final Account account;
Andy Huang9f855d62013-05-30 17:15:03 -0700496 if (hadSavedInstanceStateMessage(savedState)) {
497 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
498 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
Tony Mantler581edd42014-02-18 15:41:22 -0800499 message = savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700500
Andy Huang9f855d62013-05-30 17:15:03 -0700501 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800502 mRefMessage = savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700503 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700504
505 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700506 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700507 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700508 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
509 // Initialize the message from the message in the intent
Tony Mantler581edd42014-02-18 15:41:22 -0800510 message = intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700511 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Tony Mantler581edd42014-02-18 15:41:22 -0800512 mRefMessage = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
513 mRefMessageUri = intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
514 quotedText = null;
Andy Huang4fe0af82013-08-20 17:24:51 -0700515
516 if (Analytics.isLoggable()) {
517 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
518 Analytics.getInstance().sendEvent(
519 "notification_action", "compose", getActionString(action), 0);
520 }
521 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700522 }
Mark Wei62066e42012-09-13 12:07:02 -0700523 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700524
525 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800526 if (mAccount == null) {
527 return;
528 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700529
Andrew Sapperstein50453e42014-05-16 09:25:10 -0700530 mUseNewChips = shouldUseNewChips();
Scott Kennedyfe853d32013-06-19 11:47:35 -0700531 initRecipients();
532
Scott Kennedy5680ec22013-01-07 13:15:20 -0800533 // Clear the notification and mark the conversation as seen, if necessary
534 final Folder notificationFolder =
535 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
536 if (notificationFolder != null) {
537 final Intent clearNotifIntent =
538 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800539 clearNotifIntent.setPackage(getPackageName());
Scott Kennedy48cfe462013-04-10 11:32:02 -0700540 clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
541 clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800542
543 startService(clearNotifIntent);
544 }
545
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700546 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
547 mLaunchedFromEmail = true;
548 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
549 final Uri dataUri = intent.getData();
550 if (dataUri != null) {
551 final String dataScheme = intent.getData().getScheme();
552 final String accountScheme = mAccount.composeIntentUri.getScheme();
553 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
554 }
555 }
556
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700557 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700558 mShowQuotedText = true;
559 mComposeMode = action;
Alan Lau15490232014-03-06 14:53:14 -0800560
561 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Alan Lauc4288f22014-05-16 11:44:27 -0700562 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
Alan Lau439aa5d2014-05-27 17:57:13 -0700563 String wearReply = null;
Alan Lauc4288f22014-05-16 11:44:27 -0700564 if (remoteInput != null) {
Alan Lau439aa5d2014-05-27 17:57:13 -0700565 LogUtils.d(LOG_TAG, "Got remote input from new api");
566 CharSequence input = remoteInput.getCharSequence(
Alan Lauc4288f22014-05-16 11:44:27 -0700567 NotificationActionUtils.WEAR_REPLY_INPUT);
Alan Lau439aa5d2014-05-27 17:57:13 -0700568 if (input != null) {
569 wearReply = input.toString();
Alan Lau15490232014-03-06 14:53:14 -0800570 }
Alan Lauc4288f22014-05-16 11:44:27 -0700571 } else {
Alan Lau439aa5d2014-05-27 17:57:13 -0700572 // TODO: remove after legacy code has been removed.
573 LogUtils.d(LOG_TAG,
574 "No remote input from new api, falling back to compatibility mode");
575 ClipData clipData = intent.getClipData();
576 if (clipData != null
577 && LEGACY_WEAR_EXTRA.equals(clipData.getDescription().getLabel())) {
578 Bundle extras = clipData.getItemAt(0).getIntent().getExtras();
579 if (extras != null) {
580 wearReply = extras.getString(NotificationActionUtils.WEAR_REPLY_INPUT);
581 }
582 }
583 }
584
585 if (!TextUtils.isEmpty(wearReply)) {
586 createWearReplyTask(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION,
587 mComposeMode, wearReply).execute();
588 finish();
589 return;
590 } else {
591 LogUtils.w(LOG_TAG, "remote input string is null");
Alan Lau15490232014-03-06 14:53:14 -0800592 }
593 }
594
Alice Yanga990a712013-03-13 18:37:00 -0700595 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700596 return;
597 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700598 initFromDraftMessage(message);
599 initQuotedTextFromRefMessage(mRefMessage, action);
Alice Yanga990a712013-03-13 18:37:00 -0700600 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700601 // if we should be showing quoted text but mRefMessage is null
602 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700603 if (mShowQuotedText && mRefMessage == null) {
604 if (quotedText != null) {
605 initQuotedText(quotedText, false /* shouldQuoteText */);
606 } else if (mExtraValues != null) {
607 initExtraValues(mExtraValues);
608 return;
609 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700610 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700611 } else if (action == EDIT_DRAFT) {
Tony Mantler581edd42014-02-18 15:41:22 -0800612 if (message == null) {
613 throw new IllegalStateException("Message must not be null to edit draft");
614 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700615 initFromDraftMessage(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700616 // Update the action to the draft type of the previous draft
617 switch (message.draftType) {
618 case UIProvider.DraftType.REPLY:
619 action = REPLY;
620 break;
621 case UIProvider.DraftType.REPLY_ALL:
622 action = REPLY_ALL;
623 break;
624 case UIProvider.DraftType.FORWARD:
625 action = FORWARD;
626 break;
627 case UIProvider.DraftType.COMPOSE:
628 default:
629 action = COMPOSE;
630 break;
631 }
Alice Yanga990a712013-03-13 18:37:00 -0700632 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
633
634 mShowQuotedText = message.appendRefMessageContent;
635 if (message.refMessageUri != null) {
636 // If we're editing an existing draft that was in reference to an existing message,
637 // still need to load that original message since we might need to refer to the
638 // original sender and recipients if user switches "reply <-> reply-all".
639 mRefMessageUri = message.refMessageUri;
640 mComposeMode = action;
641 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
642 return;
643 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700644 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
645 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800646 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700647 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700648 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700649 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700650 if (initFromExtras(intent)) {
651 return;
652 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700653 }
Alice Yanga990a712013-03-13 18:37:00 -0700654
655 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700656 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700657 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700658
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -0700659 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
Alan Lau15490232014-03-06 14:53:14 -0800660 private static AsyncTask<Void, Void, Message> createWearReplyTask(
661 final ComposeActivity composeActivity,
662 final Uri refMessageUri, final String[] projection, final int action,
663 final String wearReply) {
664 return new AsyncTask<Void, Void, Message>() {
665 private Intent mEmptyServiceIntent = new Intent(composeActivity, EmptyService.class);
666
667 @Override
668 protected void onPreExecute() {
669 // Start service so we won't be killed if this app is put in the background.
670 composeActivity.startService(mEmptyServiceIntent);
671 }
672
673 @Override
674 protected Message doInBackground(Void... params) {
675 Cursor cursor = composeActivity.getContentResolver()
676 .query(refMessageUri, projection, null, null, null, null);
677 if (cursor != null) {
678 try {
679 cursor.moveToFirst();
680 return new Message(cursor);
681 } finally {
682 cursor.close();
683 }
684 }
685 return null;
686 }
687
688 @Override
689 protected void onPostExecute(Message message) {
690 composeActivity.stopService(mEmptyServiceIntent);
691
692 composeActivity.mRefMessage = message;
693 composeActivity.initFromRefMessage(action);
694 composeActivity.setBody(wearReply, false);
695 composeActivity.finishSetup(action, composeActivity.getIntent(), null);
696 composeActivity.sendOrSaveWithSanityChecks(false /* save */, true /* show toast */,
697 false /* orientationChanged */, true /* autoSend */);
698 }
699 };
700 }
701
Mindy Pereirab199d172012-08-13 11:04:03 -0700702 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700703 final Account[] allAccounts = AccountUtils.getAccounts(this);
704 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700705 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
706 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700707 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700708 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
709 }
710 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700711 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700712 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700713 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700714 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700715 anySyncing = true;
716 break;
717 }
718 }
719 if (!anySyncing) {
720 // There are accounts, but none are sync'd, which is just like having no accounts.
721 mAccounts = null;
722 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
723 return;
724 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700725 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700726 finishCreate();
727 }
728 }
729
Mindy Pereira47d0e652012-07-23 09:45:07 -0700730 private Account obtainAccount(Intent intent) {
731 Account account = null;
732 Object accountExtra = null;
733 if (intent != null && intent.getExtras() != null) {
734 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
735 if (accountExtra instanceof Account) {
736 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700737 } else if (accountExtra instanceof String) {
738 // This is the Account attached to the widget compose intent.
Tony Mantler26a20752014-02-28 16:44:24 -0800739 account = Account.newInstance((String) accountExtra);
mindyp7ae042e2012-08-27 13:27:37 -0700740 if (account != null) {
741 return account;
742 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700743 }
mindyp5ee9dc42013-01-08 09:54:54 -0800744 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
745 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
746 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700747 }
Tony Mantler581edd42014-02-18 15:41:22 -0800748
749 MailAppProvider provider = MailAppProvider.getInstance();
750 String lastAccountUri = provider.getLastSentFromAccount();
751 if (TextUtils.isEmpty(lastAccountUri)) {
752 lastAccountUri = provider.getLastViewedAccount();
Mindy Pereira47d0e652012-07-23 09:45:07 -0700753 }
Tony Mantler581edd42014-02-18 15:41:22 -0800754 if (!TextUtils.isEmpty(lastAccountUri)) {
755 accountExtra = Uri.parse(lastAccountUri);
756 }
757
Mindy Pereirab199d172012-08-13 11:04:03 -0700758 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700759 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
760 // For backwards compatibility, we need to check account
761 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700762 for (Account a : mAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -0700763 if (a.getEmailAddress().equals(accountExtra)) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700764 account = a;
765 }
766 }
767 } else if (accountExtra instanceof Uri) {
768 // The uri of the last viewed account is what is stored in
769 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700770 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700771 if (a.uri.equals(accountExtra)) {
772 account = a;
773 }
774 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700775 }
776 if (account == null) {
777 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700778 }
779 }
780 return account;
781 }
782
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700783 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800784 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700785 // Don't bother with the intent if we have procured a message from the
786 // intent already.
787 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
788 initAttachmentsFromIntent(intent);
789 }
Alice Yanga990a712013-03-13 18:37:00 -0700790 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700791 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
792 action);
mindypd4a48662012-11-08 17:13:49 -0800793
794 // If this is a draft message, the draft account is whatever account was
795 // used to open the draft message in Compose.
796 if (mDraft != null) {
797 mDraftAccount = mReplyFromAccount;
798 }
799
Mindy Pereira75f66632012-01-11 11:42:02 -0800800 initChangeListeners();
Jin Cao32973b42014-05-06 16:12:11 -0700801
802 // These two should be identical since we check CC and BCC the same way
803 boolean showCc = !TextUtils.isEmpty(mCc.getText()) || (savedInstanceState != null &&
804 savedInstanceState.getBoolean(EXTRA_SHOW_CC));
805 boolean showBcc = !TextUtils.isEmpty(mBcc.getText()) || (savedInstanceState != null &&
806 savedInstanceState.getBoolean(EXTRA_SHOW_BCC));
807 mCcBccView.show(false /* animate */, showCc, showBcc);
Mindy Pereira326689d2012-05-17 10:14:14 -0700808 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700809 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800810
Tony Mantler581edd42014-02-18 15:41:22 -0800811 mRespondedInline = mInnerSavedState != null &&
812 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE);
mindyp1623f9b2012-11-21 12:41:16 -0800813 if (mRespondedInline) {
814 mQuotedTextView.setVisibility(View.GONE);
815 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700816 }
817
Scott Kennedyff8553f2013-04-05 20:57:44 -0700818 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700819 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
820 }
821
Mindy Pereira71c9e562012-05-17 11:01:02 -0700822 private void updateHideOrShowQuotedText(boolean showQuotedText) {
823 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700824 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700825 }
826
827 private void setFocus(int action) {
828 if (action == EDIT_DRAFT) {
829 int type = mDraft.draftType;
830 switch (type) {
831 case UIProvider.DraftType.COMPOSE:
832 case UIProvider.DraftType.FORWARD:
833 action = COMPOSE;
834 break;
835 case UIProvider.DraftType.REPLY:
836 case UIProvider.DraftType.REPLY_ALL:
837 default:
838 action = REPLY;
839 break;
840 }
841 }
842 switch (action) {
843 case FORWARD:
844 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800845 if (TextUtils.isEmpty(mTo.getText())) {
846 mTo.requestFocus();
847 break;
848 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700849 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700850 case REPLY:
851 case REPLY_ALL:
852 default:
853 focusBody();
854 break;
855 }
856 }
857
858 /**
859 * Focus the body of the message.
860 */
Tony Mantler6a7ac782014-02-19 15:22:02 -0800861 private void focusBody() {
Mindy Pereira433b1982012-04-03 11:53:07 -0700862 mBodyView.requestFocus();
Tony Mantler6a7ac782014-02-19 15:22:02 -0800863 resetBodySelection();
864 }
Mindy Pereira433b1982012-04-03 11:53:07 -0700865
Tony Mantler6a7ac782014-02-19 15:22:02 -0800866 private void resetBodySelection() {
867 int length = mBodyView.getText().length();
Mindy Pereira433b1982012-04-03 11:53:07 -0700868 int signatureStartPos = getSignatureStartPosition(
869 mSignature, mBodyView.getText().toString());
870 if (signatureStartPos > -1) {
871 // In case the user deleted the newlines...
872 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700873 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700874 // Move cursor to the end.
875 mBodyView.setSelection(length);
876 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800877 }
878
879 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700880 protected void onStart() {
881 super.onStart();
882
883 Analytics.getInstance().activityStart(this);
884 }
885
886 @Override
887 protected void onStop() {
888 super.onStop();
889
890 Analytics.getInstance().activityStop(this);
891 }
892
893 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800894 protected void onResume() {
895 super.onResume();
896 // Update the from spinner as other accounts
897 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800898 if (mFromSpinner != null && mAccount != null) {
Andrew Sappersteina01ddca2014-03-04 10:59:56 -0800899 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800900 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800901 }
902
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800903 @Override
904 protected void onPause() {
905 super.onPause();
906
Mindy Pereiraa2148332012-07-02 13:54:14 -0700907 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700908 // Don't save unnecessary drafts if we are only changing the orientation.
909 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700910 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700911
Andy Huange003b4c2013-08-16 10:32:05 -0700912 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700913 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
914 // because that method can be invoked many times in a single compose session.)
915 logSendOrSave(true /* save */);
916 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700917 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800918 }
919
920 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700921 protected void onActivityResult(int request, int result, Intent data) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700922 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800923 addAttachmentAndUpdateView(data);
Mindy Pereirab199d172012-08-13 11:04:03 -0700924 mAddingAttachment = false;
925 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700926 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700927 if (result != RESULT_OK) {
928 finish();
929 } else {
930 // Watch for accounts to show up!
931 // restart the loader to get the updated list of accounts
932 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
933 showWaitFragment(null);
934 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800935 }
936 }
937
938 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700939 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700940 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
941 if (hasAccounts) {
942 clearChangeListeners();
943 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700944 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700945 if (mInnerSavedState != null) {
946 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
947 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
948 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700949 // There should be a focus and it should be an EditText since we
950 // only save these extras if these conditions are true.
951 EditText focusEditText = (EditText) getCurrentFocus();
952 final int length = focusEditText.getText().length();
953 if (selectionStart < length && selectionEnd < length) {
954 focusEditText.setSelection(selectionStart, selectionEnd);
955 }
956 }
957 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700958 if (hasAccounts) {
959 initChangeListeners();
960 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700961 }
962
963 @Override
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -0700964 protected void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800965 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700966 final Bundle inner = new Bundle();
967 saveState(inner);
968 state.putBundle(KEY_INNER_SAVED_STATE, inner);
969 }
970
971 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700972 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
973 if (mAccounts == null || mAccounts.length == 0) {
974 return;
975 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700976 // The framework is happy to save and restore the selection but only if it also saves and
977 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
978 // this manually.
979 View focus = getCurrentFocus();
980 if (focus != null && focus instanceof EditText) {
981 EditText focusEditText = (EditText) focus;
982 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
983 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
984 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700985
986 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700987 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700988 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
989 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
990 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700991 if (selectedReplyFromAccount != null) {
992 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
993 .toString());
994 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
995 } else {
996 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
997 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800998
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700999 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
1000 // We don't have a draft id, and we have a request id,
1001 // save the request id.
1002 state.putInt(EXTRA_REQUEST_ID, mRequestId);
1003 }
1004
1005 // We want to restore the current mode after a pause
1006 // or rotation.
1007 int mode = getMode();
1008 state.putInt(EXTRA_ACTION, mode);
1009
Anthony Lee2a3cc132014-04-22 14:15:25 -07001010 final Message message = createMessage(selectedReplyFromAccount, mRefMessage, mode);
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001011 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -08001012 message.id = mDraft.id;
1013 message.serverId = mDraft.serverId;
1014 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001015 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001016 state.putParcelable(EXTRA_MESSAGE, message);
1017
1018 if (mRefMessage != null) {
1019 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001020 } else if (message.appendRefMessageContent) {
1021 // If we have no ref message but should be appending
1022 // ref message content, we have orphaned quoted text. Save it.
1023 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001024 }
Mindy Pereira326689d2012-05-17 10:14:14 -07001025 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
1026 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -08001027 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -08001028 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -07001029 state.putParcelableArrayList(
1030 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -07001031
1032 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001033 }
1034
1035 private int getMode() {
1036 int mode = ComposeActivity.COMPOSE;
1037 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001038 if (actionBar != null
1039 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001040 mode = actionBar.getSelectedNavigationIndex();
1041 }
1042 return mode;
1043 }
1044
Anthony Lee2a3cc132014-04-22 14:15:25 -07001045 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, Message refMessage,
1046 int mode) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001047 Message message = new Message();
1048 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -07001049 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001050 message.uri = null;
1051 message.conversationUri = null;
1052 message.subject = mSubject.getText().toString();
1053 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001054 message.setTo(formatSenders(mTo.getText().toString()));
1055 message.setCc(formatSenders(mCc.getText().toString()));
1056 message.setBcc(formatSenders(mBcc.getText().toString()));
1057 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001058 message.dateReceivedMs = 0;
Andy Huang91ede362014-01-21 19:16:00 -08001059 message.bodyHtml = spannedBodyToHtml(mBodyView.getText());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001060 message.bodyText = mBodyView.getText().toString();
1061 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -07001062 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001063 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
1064 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
1065 message.hasAttachments = attachments != null && attachments.size() > 0;
1066 message.attachmentListUri = null;
1067 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001068 message.alwaysShowImages = false;
1069 message.attachmentsJson = Attachment.toJSONArray(attachments);
1070 CharSequence quotedText = mQuotedTextView.getQuotedText();
Anthony Lee2a3cc132014-04-22 14:15:25 -07001071 message.quotedTextOffset = -1; // Just a default value.
1072 if (refMessage != null && !TextUtils.isEmpty(quotedText)) {
1073 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1074 // We want the index to point to just the quoted text and not the
1075 // "On December 25, 2014..." part of it.
1076 message.quotedTextOffset =
1077 QuotedTextView.getQuotedTextOffset(quotedText.toString());
1078 } else if (!TextUtils.isEmpty(refMessage.bodyText)) {
1079 // We want to point to the entire quoted text.
1080 message.quotedTextOffset = QuotedTextView.findQuotedTextIndex(quotedText);
1081 }
1082 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001083 message.accountUri = null;
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001084 final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
1085 : mAccount != null ? mAccount.getEmailAddress() : null;
Tony Mantlerf441d142013-10-22 11:46:00 -07001086 final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
1087 : mAccount != null ? mAccount.getSenderName() : null;
Tony Mantler821e5782014-01-06 15:33:43 -08001088 final Address address = new Address(email, senderName);
Tony Mantlerf441d142013-10-22 11:46:00 -07001089 message.setFrom(address.toHeader());
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001090 message.draftType = getDraftType(mode);
mindype7b76aa2012-11-14 16:19:13 -08001091 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001092 }
1093
Scott Kennedyff8553f2013-04-05 20:57:44 -07001094 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -07001095 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
1096 return string.substring(0, string.length() - 1);
1097 }
1098 return string;
1099 }
1100
Mindy Pereira818143e2012-01-11 13:59:49 -08001101 @VisibleForTesting
Andy Huang91ede362014-01-21 19:16:00 -08001102 protected void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -07001103 if (account == null) {
1104 return;
1105 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001106 if (!account.equals(mAccount)) {
1107 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001108 mCachedSettings = mAccount.settings;
1109 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -07001110 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001111 if (mAccount != null) {
Tony Mantler79b11562013-10-09 15:31:50 -07001112 MailActivity.setNfcMessage(mAccount.getEmailAddress());
Mindy Pereirafa20c1a2012-07-23 13:00:02 -07001113 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001114 }
1115
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001116 private void initFromSpinner(Bundle bundle, int action) {
1117 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001118 action = COMPOSE;
1119 }
Andrew Sappersteina01ddca2014-03-04 10:59:56 -08001120 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001121
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001122 if (bundle != null) {
1123 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
1124 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
1125 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
1126 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001127 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001128 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
1129 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001130 }
1131 if (mReplyFromAccount == null) {
1132 if (mDraft != null) {
1133 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
1134 } else if (mRefMessage != null) {
1135 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
1136 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001137 }
1138 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -07001139 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001140 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001141
Mindy Pereira62de1b12012-04-06 12:17:56 -07001142 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001143
Mindy Pereira62de1b12012-04-06 12:17:56 -07001144 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001145 // If there is only 1 account, just show that account.
1146 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001147 // send mail from / save drafts to.
1148 mFromStatic.setVisibility(View.GONE);
Andy Huangca4676f2014-01-16 13:22:20 -08001149 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001150 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001151 } else {
1152 mFromStatic.setVisibility(View.VISIBLE);
Andy Huangca4676f2014-01-16 13:22:20 -08001153 mFromStaticText.setText(mReplyFromAccount.address);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001154 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001155 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001156 }
1157
Mindy Pereira62de1b12012-04-06 12:17:56 -07001158 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1159 if (refMessage.accountUri != null) {
1160 // This must be from combined inbox.
1161 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1162 for (ReplyFromAccount from : replyFromAccounts) {
1163 if (from.account.uri.equals(refMessage.accountUri)) {
1164 return from;
1165 }
1166 }
1167 return null;
1168 } else {
1169 return getReplyFromAccount(account, refMessage);
1170 }
1171 }
1172
1173 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001174 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001175 * return who the message should be sent from.
1176 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001177 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001178 * @return the address from which to reply.
1179 */
1180 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1181 // First see if we are supposed to use the default address or
1182 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001183 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001184 return getDefaultReplyFromAccount(account);
1185 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001186 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001187 // all the message recipients and find one that matches
1188 // a custom from or account.
1189 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001190 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1191 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001192 return getMatchingRecipient(account, allRecipients);
1193 }
1194 }
1195
1196 /**
1197 * Compare all the recipients of an email to the current account and all
1198 * custom addresses associated with that account. Return the match if there
1199 * is one, or the default account if there isn't.
1200 */
1201 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1202 // Tokenize the list and place in a hashmap.
1203 ReplyFromAccount matchingReplyFrom = null;
1204 Rfc822Token[] tokens;
1205 HashSet<String> recipientsMap = new HashSet<String>();
1206 for (String address : sentTo) {
1207 tokens = Rfc822Tokenizer.tokenize(address);
Tony Mantler581edd42014-02-18 15:41:22 -08001208 for (final Rfc822Token token : tokens) {
1209 recipientsMap.add(token.getAddress());
Mindy Pereira62de1b12012-04-06 12:17:56 -07001210 }
1211 }
1212
1213 int matchingAddressCount = 0;
1214 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001215 customFroms = account.getReplyFroms();
1216 if (customFroms != null) {
1217 for (ReplyFromAccount entry : customFroms) {
1218 if (recipientsMap.contains(entry.address)) {
1219 matchingReplyFrom = entry;
1220 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001221 }
1222 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001223 }
1224 if (matchingAddressCount > 1) {
1225 matchingReplyFrom = getDefaultReplyFromAccount(account);
1226 }
1227 return matchingReplyFrom;
1228 }
1229
Scott Kennedyff8553f2013-04-05 20:57:44 -07001230 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1231 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001232 if (from.isDefault) {
1233 return from;
1234 }
1235 }
Tony Mantlerf441d142013-10-22 11:46:00 -07001236 return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1237 account.getSenderName(), account.getEmailAddress(), true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001238 }
1239
Tony Mantlerf441d142013-10-22 11:46:00 -07001240 private ReplyFromAccount getReplyFromAccountFromDraft(final Account account,
1241 final Message msg) {
1242 final Address[] draftFroms = Address.parse(msg.getFrom());
1243 final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
Mindy Pereira62de1b12012-04-06 12:17:56 -07001244 ReplyFromAccount replyFromAccount = null;
1245 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Tony Mantler79b11562013-10-09 15:31:50 -07001246 if (TextUtils.equals(account.getEmailAddress(), sender)) {
Tony Mantlerf441d142013-10-22 11:46:00 -07001247 replyFromAccount = getDefaultReplyFromAccount(account);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001248 } else {
1249 for (ReplyFromAccount fromAccount : replyFromAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -07001250 if (TextUtils.equals(fromAccount.address, sender)) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001251 replyFromAccount = fromAccount;
1252 break;
1253 }
1254 }
1255 }
1256 return replyFromAccount;
1257 }
1258
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001259 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001260 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001261 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001262 if (mCcBccButton != null) {
1263 mCcBccButton.setOnClickListener(this);
1264 }
1265 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001266 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Tony Mantler581edd42014-02-18 15:41:22 -08001267 final View addAttachmentsButton = findViewById(R.id.add_attachment);
1268 if (addAttachmentsButton != null) {
1269 addAttachmentsButton.setOnClickListener(this);
mindypcd0b0b92012-08-23 14:33:17 -07001270 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001271 mTo = (RecipientEditTextView) findViewById(R.id.to);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001272 initializeRecipientEditTextView(mTo);
Mindy Pereira818143e2012-01-11 13:59:49 -08001273 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001274 initializeRecipientEditTextView(mCc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001275 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001276 initializeRecipientEditTextView(mBcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001277 // TODO: add special chips text change watchers before adding
1278 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001279 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001280 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001281 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1282 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001283 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001284 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001285 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001286 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001287 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001288 }
1289
Andrew Sapperstein09da9422014-05-30 09:48:08 -07001290 private void initializeRecipientEditTextView(RecipientEditTextView view) {
1291 view.setTokenizer(new Rfc822Tokenizer());
1292 view.setThreshold(COMPLETION_THRESHOLD);
1293 }
1294
mindyp62d3ec72012-08-24 13:04:09 -07001295 @Override
1296 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1297 if (action == EditorInfo.IME_ACTION_DONE) {
1298 focusBody();
1299 return true;
1300 }
1301 return false;
1302 }
1303
Andy Huang91ede362014-01-21 19:16:00 -08001304 /**
1305 * Convert the body text (in {@link Spanned} form) to ready-to-send HTML format as a plain
1306 * String.
1307 *
1308 * @param body the body text including fancy style spans
1309 * @return HTML formatted body that's suitable for sending or saving
1310 */
1311 private String spannedBodyToHtml(Spanned body) {
1312 final HtmlifyBeginResult r = onHtmlifyBegin(removeComposingSpans(body));
1313 return onHtmlifyEnd(Html.toHtml(r.result), r.extras);
1314 }
1315
1316 /**
1317 * A hook for subclasses to convert custom spans in the body text prior to system HTML
1318 * conversion. That HTML conversion is lossy, so anything above and beyond its capability
1319 * has to be handled here.
1320 *
1321 * @param body
1322 * @return a copy of the body text with custom spans replaced with HTML
1323 */
1324 protected HtmlifyBeginResult onHtmlifyBegin(Spanned body) {
1325 return new HtmlifyBeginResult(body, null /* extras */);
1326 }
1327
1328 protected String onHtmlifyEnd(String html, Object extras) {
1329 return html;
1330 }
1331
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001332 protected TextView getBody() {
1333 return mBodyView;
1334 }
1335
1336 @VisibleForTesting
Andy Huang0a2a3462013-12-20 15:56:13 -08001337 public String getBodyHtml() {
Andy Huang91ede362014-01-21 19:16:00 -08001338 return spannedBodyToHtml(mBodyView.getText());
Andy Huang0a2a3462013-12-20 15:56:13 -08001339 }
1340
1341 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001342 public Account getFromAccount() {
1343 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1344 mReplyFromAccount.account : mAccount;
1345 }
1346
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001347 private void clearChangeListeners() {
1348 mSubject.removeTextChangedListener(this);
1349 mBodyView.removeTextChangedListener(this);
1350 mTo.removeTextChangedListener(mToListener);
1351 mCc.removeTextChangedListener(mCcListener);
1352 mBcc.removeTextChangedListener(mBccListener);
1353 mFromSpinner.setOnAccountChangedListener(null);
1354 mAttachmentsView.setAttachmentChangesListener(null);
1355 }
1356
Mindy Pereira75f66632012-01-11 11:42:02 -08001357 // Now that the message has been initialized from any existing draft or
1358 // ref message data, set up listeners for any changes that occur to the
1359 // message.
1360 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001361 // Make sure we only add text changed listeners once!
1362 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001363 mSubject.addTextChangedListener(this);
1364 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001365 if (mToListener == null) {
1366 mToListener = new RecipientTextWatcher(mTo, this);
1367 }
1368 mTo.addTextChangedListener(mToListener);
1369 if (mCcListener == null) {
1370 mCcListener = new RecipientTextWatcher(mCc, this);
1371 }
1372 mCc.addTextChangedListener(mCcListener);
1373 if (mBccListener == null) {
1374 mBccListener = new RecipientTextWatcher(mBcc, this);
1375 }
1376 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001377 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001378 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001379 }
1380
Alice Yanga990a712013-03-13 18:37:00 -07001381 private void initActionBar() {
1382 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001383 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001384 if (actionBar == null) {
1385 return;
1386 }
Alice Yanga990a712013-03-13 18:37:00 -07001387 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001388 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1389 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001390 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001391 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001392 if (mComposeModeAdapter == null) {
1393 mComposeModeAdapter = new ComposeModeAdapter(this);
1394 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001395 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1396 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001397 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001398 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001399 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001400 break;
1401 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001402 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001403 break;
1404 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001405 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001406 break;
1407 }
1408 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001409 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1410 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1411 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001412 }
1413
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001414 private void initFromRefMessage(int action) {
1415 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001416
1417 // Check if To: address and email body needs to be prefilled based on extras.
1418 // This is used for reporting rendering feedback.
1419 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1420 Intent intent = getIntent();
1421 if (intent.getExtras() != null) {
1422 String toAddresses = intent.getStringExtra(EXTRA_TO);
1423 if (toAddresses != null) {
1424 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1425 }
1426 String body = intent.getStringExtra(EXTRA_BODY);
1427 if (body != null) {
1428 setBody(body, false /* withSignature */);
1429 }
1430 }
1431 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001432 }
1433
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001434 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001435 setSubject(mRefMessage, action);
1436 // Setup recipients
1437 if (action == FORWARD) {
1438 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001439 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001440 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001441 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001442 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1443 initAttachments(mRefMessage);
1444 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001445 }
1446
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001447 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001448 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001449
1450 mDraft = message;
1451 mDraftId = message.id;
1452 mSubject.setText(message.subject);
1453 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001454 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001455 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001456 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1457 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001458 if (message.hasAttachments) {
1459 List<Attachment> attachments = message.getAttachments();
1460 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001461 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001462 }
1463 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07001464 int quotedTextIndex = message.appendRefMessageContent ? message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001465 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001466 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001467 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001468 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001469 if (quotedTextIndex > -1) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07001470 // Find the offset in the html text of the actual quoted text and strip it out.
1471 // Note that the actual quotedTextOffset in the message has not changed as
1472 // this different offset is used only for display purposes. They point to different
1473 // parts of the original message. Please see the comments in QuoteTextView
1474 // to see the differences.
Mindy Pereira752222d2012-07-19 09:58:53 -07001475 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1476 if (quotedTextIndex > -1) {
mindypc59dd822012-11-13 10:56:21 -08001477 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1478 quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001479 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1480 message.bodyHtml.length());
1481 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001482 } else {
mindypc59dd822012-11-13 10:56:21 -08001483 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001484 }
1485 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001486 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001487 final String body = message.bodyText;
Anthony Lee2a3cc132014-04-22 14:15:25 -07001488 final CharSequence bodyText;
1489 if (TextUtils.isEmpty(body)) {
1490 bodyText = "";
1491 quotedText = null;
1492 } else {
1493 if (quotedTextIndex > body.length()) {
1494 // Sanity check to guarantee that we will not over index the String.
1495 // If this happens there is a bigger problem. This should never happen hence
1496 // the wtf logging.
1497 quotedTextIndex = -1;
1498 LogUtils.wtf(LOG_TAG, "quotedTextIndex (%d) > body.length() (%d)",
1499 quotedTextIndex, body.length());
1500 }
1501 bodyText = quotedTextIndex > -1 ? body.substring(0, quotedTextIndex) : body;
1502 if (quotedTextIndex > -1) {
1503 quotedText = body.substring(quotedTextIndex);
1504 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001505 }
1506 mBodyView.setText(bodyText);
1507 }
1508 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001509 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001510 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001511 }
1512
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001513 /**
1514 * Fill all the widgets with the content found in the Intent Extra, if any.
1515 * Also apply the same style to all widgets. Note: if initFromExtras is
1516 * called as a result of switching between reply, reply all, and forward per
1517 * the latest revision of Gmail, and the user has already made changes to
1518 * attachments on a previous incarnation of the message (as a reply, reply
1519 * all, or forward), the original attachments from the message will not be
1520 * re-instantiated. The user's changes will be respected. This follows the
1521 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001522 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001523 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001524 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001525 // If we were invoked with a SENDTO intent, the value
1526 // should take precedence
1527 final Uri dataUri = intent.getData();
1528 if (dataUri != null) {
1529 if (MAIL_TO.equals(dataUri.getScheme())) {
1530 initFromMailTo(dataUri.toString());
1531 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001532 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001533 String toText = dataUri.getSchemeSpecificPart();
1534 if (toText != null) {
1535 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001536 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001537 }
1538 }
1539 }
1540 }
1541
1542 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1543 if (extraStrings != null) {
1544 addToAddresses(Arrays.asList(extraStrings));
1545 }
1546 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1547 if (extraStrings != null) {
1548 addCcAddresses(Arrays.asList(extraStrings), null);
1549 }
1550 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1551 if (extraStrings != null) {
1552 addBccAddresses(Arrays.asList(extraStrings));
1553 }
1554
1555 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1556 if (extraString != null) {
1557 mSubject.setText(extraString);
1558 }
1559
1560 for (String extra : ALL_EXTRAS) {
1561 if (intent.hasExtra(extra)) {
1562 String value = intent.getStringExtra(extra);
1563 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001564 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001565 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001566 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001567 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001568 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001569 } else if (EXTRA_SUBJECT.equals(extra)) {
1570 mSubject.setText(value);
1571 } else if (EXTRA_BODY.equals(extra)) {
1572 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001573 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1574 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001575 }
1576 }
1577 }
1578
1579 Bundle extras = intent.getExtras();
1580 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001581 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1582 if (text != null) {
1583 setBody(text, true /* with signature */);
1584 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001585
1586 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001587 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001588
1589 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1590 if (mExtraValues != null) {
1591 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1592 initExtraValues(mExtraValues);
1593 return true;
1594 }
1595
1596 return false;
1597 }
1598
1599 protected void initExtraValues(ContentValues extraValues) {
1600 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001601 }
1602
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001603
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001604 @VisibleForTesting
1605 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001606 // TODO: handle the case where there are spaces in the display name as
1607 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1608 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001609 // Since URLDecode.decode changes + into ' ', and + is a valid
1610 // email character, we need to find/ replace these ourselves before
1611 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001612 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001613 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001614 } catch (IllegalArgumentException e) {
1615 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1616 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1617 } else {
1618 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1619 }
1620 return null;
1621 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001622 }
1623
1624 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001625 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1626 * changing '+' into ' '
1627 *
1628 * @param toReplace Input string
1629 * @return The string with all "+" characters replaced with "%2B"
1630 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001631 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001632 return toReplace.replace("+", "%2B");
1633 }
1634
1635 /**
Jin Caod67d7e32014-03-26 16:49:48 -07001636 * Replaces all occurrences of '%' with "%25", to prevent URLDecode.decode from
1637 * crashing on decoded '%' symbols
1638 *
1639 * @param toReplace Input string
1640 * @return The string with all "%" characters replaced with "%25"
1641 */
1642 private static String replacePercent(String toReplace) {
1643 return toReplace.replace("%", "%25");
1644 }
1645
1646 /**
1647 * Helper function to encapsulate encoding/decoding string from Uri.getQueryParameters
1648 * @param content Input string
1649 * @return The string that's properly escaped to be shown in mail subject/content
1650 */
1651 private static String decodeContentFromQueryParam(String content) {
1652 try {
1653 return URLDecoder.decode(replacePlus(replacePercent(content)), UTF8_ENCODING_NAME);
1654 } catch (UnsupportedEncodingException e) {
1655 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), content);
1656 return ""; // Default to empty string so setText/setBody has same behavior as before.
1657 }
1658 }
1659
1660 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001661 * Initialize the compose view from a String representing a mailTo uri.
1662 * @param mailToString The uri as a string.
1663 */
1664 public void initFromMailTo(String mailToString) {
1665 // We need to disguise this string as a URI in order to parse it
1666 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1667 Uri uri = Uri.parse("foo://" + mailToString);
1668 int index = mailToString.indexOf("?");
1669 int length = "mailto".length() + 1;
1670 String to;
1671 try {
1672 // Extract the recipient after mailto:
1673 if (index == -1) {
1674 to = decodeEmailInUri(mailToString.substring(length));
1675 } else {
1676 to = decodeEmailInUri(mailToString.substring(length, index));
1677 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001678 if (!TextUtils.isEmpty(to)) {
1679 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1680 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001681 } catch (UnsupportedEncodingException e) {
1682 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1683 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1684 } else {
1685 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1686 }
1687 }
1688
1689 List<String> cc = uri.getQueryParameters("cc");
1690 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1691
1692 List<String> otherTo = uri.getQueryParameters("to");
1693 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1694
1695 List<String> bcc = uri.getQueryParameters("bcc");
1696 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1697
Jin Caod67d7e32014-03-26 16:49:48 -07001698 // NOTE: Uri.getQueryParameters already decodes % encoded characters
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001699 List<String> subject = uri.getQueryParameters("subject");
1700 if (subject.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001701 mSubject.setText(decodeContentFromQueryParam(subject.get(0)));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001702 }
1703
1704 List<String> body = uri.getQueryParameters("body");
1705 if (body.size() > 0) {
Jin Caod67d7e32014-03-26 16:49:48 -07001706 setBody(decodeContentFromQueryParam(body.get(0)), true /* with signature */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001707 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001708 }
1709
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001710 @VisibleForTesting
1711 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001712 addAttachments(refMessage.getAttachments());
1713 }
1714
1715 public long addAttachments(List<Attachment> attachments) {
1716 long size = 0;
1717 AttachmentFailureException error = null;
1718 for (Attachment a : attachments) {
1719 try {
1720 size += mAttachmentsView.addAttachment(mAccount, a);
1721 } catch (AttachmentFailureException e) {
1722 error = e;
1723 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001724 }
Mark Wei434f2942012-08-24 11:54:02 -07001725 if (error != null) {
1726 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1727 if (attachments.size() > 1) {
1728 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1729 } else {
1730 showAttachmentTooBigToast(error.getErrorRes());
1731 }
1732 }
1733 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001734 }
1735
1736 /**
1737 * When an attachment is too large to be added to a message, show a toast.
1738 * This method also updates the position of the toast so that it is shown
1739 * clearly above they keyboard if it happens to be open.
1740 */
Mark Wei434f2942012-08-24 11:54:02 -07001741 private void showAttachmentTooBigToast(int errorRes) {
1742 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1743 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1744 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001745 }
1746
Mark Wei434f2942012-08-24 11:54:02 -07001747 private void showErrorToast(String message) {
1748 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1749 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001750 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1751 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1752 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001753 }
1754
Paul Westbrookf97588b2012-03-20 11:11:37 -07001755 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001756 Bundle extras = intent.getExtras();
1757 if (extras == null) {
1758 extras = Bundle.EMPTY;
1759 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001760 final String action = intent.getAction();
1761 if (!mAttachmentsChanged) {
1762 long totalSize = 0;
1763 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1764 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1765 for (String uriString : uris) {
1766 final Uri uri = Uri.parse(uriString);
1767 long size = 0;
1768 try {
Andy Huang91ede362014-01-21 19:16:00 -08001769 if (handleSpecialAttachmentUri(uri)) {
1770 continue;
1771 }
1772
Andy Huange003b4c2013-08-16 10:32:05 -07001773 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1774 size = mAttachmentsView.addAttachment(mAccount, a);
1775
1776 Analytics.getInstance().sendEvent("send_intent_attachment",
1777 Utils.normalizeMimeType(a.getContentType()), null, size);
1778
Paul Westbrookf97588b2012-03-20 11:11:37 -07001779 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001780 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001781 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001782 }
1783 totalSize += size;
1784 }
1785 }
mindyp9a9e8d62012-10-03 12:24:07 -07001786 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1787 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
Andy Huang91ede362014-01-21 19:16:00 -08001788 final ArrayList<Uri> uris = extras
mindyp9a9e8d62012-10-03 12:24:07 -07001789 .getParcelableArrayList(Intent.EXTRA_STREAM);
1790 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
Andy Huang91ede362014-01-21 19:16:00 -08001791 for (Uri uri : uris) {
mindyp9a9e8d62012-10-03 12:24:07 -07001792 try {
Andy Huang91ede362014-01-21 19:16:00 -08001793 if (handleSpecialAttachmentUri(uri)) {
1794 continue;
1795 }
1796
1797 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
Andy Huange003b4c2013-08-16 10:32:05 -07001798 attachments.add(a);
1799
1800 Analytics.getInstance().sendEvent("send_intent_attachment",
1801 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1802
mindyp9a9e8d62012-10-03 12:24:07 -07001803 } catch (AttachmentFailureException e) {
1804 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1805 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1806 getApplicationContext(),
1807 mAccount.settings.getMaxAttachmentSize());
1808 showErrorToast(getString
1809 (R.string.generic_attachment_problem, maxSize));
1810 }
1811 }
1812 totalSize += addAttachments(attachments);
1813 } else {
Tony Mantler581edd42014-02-18 15:41:22 -08001814 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
mindyp9a9e8d62012-10-03 12:24:07 -07001815 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001816 try {
Andy Huang91ede362014-01-21 19:16:00 -08001817 if (!handleSpecialAttachmentUri(uri)) {
1818 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1819 size = mAttachmentsView.addAttachment(mAccount, a);
Andy Huange003b4c2013-08-16 10:32:05 -07001820
Andy Huang91ede362014-01-21 19:16:00 -08001821 Analytics.getInstance().sendEvent("send_intent_attachment",
1822 Utils.normalizeMimeType(a.getContentType()), null, size);
1823 }
Andy Huange003b4c2013-08-16 10:32:05 -07001824
Paul Westbrookf97588b2012-03-20 11:11:37 -07001825 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001826 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001827 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001828 }
mindyp9a9e8d62012-10-03 12:24:07 -07001829 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001830 }
1831 }
1832
1833 if (totalSize > 0) {
1834 mAttachmentsChanged = true;
1835 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001836
1837 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1838 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001839 }
1840 }
1841 }
1842
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001843 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001844 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1845 mShowQuotedText = true;
1846 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001847
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001848 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1849 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001850 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1851 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001852 }
1853
1854 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001855 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001856 boolean ccVisible = mCcBccView.isCcVisible();
1857 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001858 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001859 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001860 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001861 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001862 : R.string.add_bcc_label));
1863 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001864 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001865 }
1866 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001867 }
1868
Mindy Pereira013194c2012-01-06 15:09:33 -08001869 /**
1870 * Add attachment and update the compose area appropriately.
Mindy Pereira013194c2012-01-06 15:09:33 -08001871 */
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001872 private void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07001873 if (data == null) {
1874 return;
1875 }
1876
1877 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1878 final ClipData clipData = data.getClipData();
1879 if (clipData != null) {
1880 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1881 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1882 }
1883 return;
1884 }
1885 }
1886
1887 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07001888 }
1889
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001890 private void addAttachmentAndUpdateView(Uri contentUri) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001891 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001892 return;
1893 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001894 try {
Andy Huang91ede362014-01-21 19:16:00 -08001895
1896 if (handleSpecialAttachmentUri(contentUri)) {
1897 return;
1898 }
1899
Andy Huang5c5fd572012-04-08 18:19:29 -07001900 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1901 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001902 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001903 showErrorToast(getResources().getString(
1904 e.getErrorRes(),
1905 AttachmentUtils.convertToHumanReadableSize(
1906 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001907 }
1908 }
1909
Andy Huang91ede362014-01-21 19:16:00 -08001910 /**
1911 * Allow subclasses to implement custom handling of attachments.
1912 *
1913 * @param contentUri a passed-in URI from a pick intent
1914 * @return true iff handled
1915 */
1916 protected boolean handleSpecialAttachmentUri(final Uri contentUri) {
1917 return false;
1918 }
1919
Andrew Sapperstein865ae9c2014-02-10 18:23:48 -08001920 private void addAttachmentAndUpdateView(Attachment attachment) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001921 try {
Mark Wei434f2942012-08-24 11:54:02 -07001922 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001923 if (size > 0) {
1924 mAttachmentsChanged = true;
1925 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001926 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001927 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001928 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001929 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001930 }
1931 }
1932
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001933 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001934 // Don't populate the address if this is a forward.
1935 if (action == ComposeActivity.FORWARD) {
1936 return;
1937 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001938 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001939 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001940
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001941 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1942 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001943 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001944 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001945 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001946 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001947 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1948 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
Andy Huange2af8872014-01-16 12:36:27 -08001949 final String[] replyToAddresses = getReplyToAddresses(
1950 refMessage.getReplyToAddressesUnescaped(), fromAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001951
1952 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1953 // Cc list is the union of the To and Cc recipients of the original
1954 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001955 // already on the To list.
1956 if (action == ComposeActivity.REPLY) {
Tony Mantler24f116f2014-01-16 10:20:50 -08001957 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001958 addToAddresses(toAddresses);
1959 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001960 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08001961 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001962 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001963 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001964 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001965 addCcAddresses(ccAddresses, toAddresses);
1966 }
1967 }
1968
Andy Huange2af8872014-01-16 12:36:27 -08001969 // If there is no reply to address, the reply to address is the sender.
1970 private static String[] getReplyToAddresses(String[] replyTo, String from) {
1971 boolean hasReplyTo = false;
1972 for (final String replyToAddress : replyTo) {
1973 if (!TextUtils.isEmpty(replyToAddress)) {
1974 hasReplyTo = true;
1975 }
1976 }
1977 return hasReplyTo ? replyTo : new String[] {from};
1978 }
1979
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001980 private void addToAddresses(Collection<String> addresses) {
1981 addAddressesToList(addresses, mTo);
1982 }
1983
1984 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001985 addCcAddressesToList(tokenizeAddressList(addresses),
1986 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001987 }
1988
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001989 private void addBccAddresses(Collection<String> addresses) {
1990 addAddressesToList(addresses, mBcc);
1991 }
1992
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001993 @VisibleForTesting
1994 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1995 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1996 String address;
1997
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001998 if (compareToList == null) {
Tony Mantler581edd42014-02-18 15:41:22 -08001999 for (final Rfc822Token[] tokens : addresses) {
2000 for (final Rfc822Token token : tokens) {
2001 address = token.toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002002 list.append(address + END_TOKEN);
2003 }
2004 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002005 } else {
2006 HashSet<String> compareTo = convertToHashSet(compareToList);
Tony Mantler581edd42014-02-18 15:41:22 -08002007 for (final Rfc822Token[] tokens : addresses) {
2008 for (final Rfc822Token token : tokens) {
2009 address = token.toString();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002010 // Check if this is a duplicate:
Tony Mantler581edd42014-02-18 15:41:22 -08002011 if (!compareTo.contains(token.getAddress())) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002012 // Get the address here
2013 list.append(address + END_TOKEN);
2014 }
2015 }
2016 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002017 }
2018 }
2019
Scott Kennedyff8553f2013-04-05 20:57:44 -07002020 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
2021 final HashSet<String> hash = new HashSet<String>();
2022 for (final Rfc822Token[] tokens : list) {
Tony Mantler581edd42014-02-18 15:41:22 -08002023 for (final Rfc822Token token : tokens) {
2024 hash.add(token.getAddress());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002025 }
2026 }
2027 return hash;
2028 }
2029
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002030 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
2031 @VisibleForTesting
2032 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
2033
2034 for (String address: addresses) {
2035 tokenized.add(Rfc822Tokenizer.tokenize(address));
2036 }
2037 return tokenized;
2038 }
2039
2040 @VisibleForTesting
2041 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
2042 for (String address : addresses) {
2043 addAddressToList(address, list);
2044 }
2045 }
2046
Scott Kennedyff8553f2013-04-05 20:57:44 -07002047 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002048 if (address == null || list == null)
2049 return;
2050
Scott Kennedyff8553f2013-04-05 20:57:44 -07002051 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002052
Tony Mantler581edd42014-02-18 15:41:22 -08002053 for (final Rfc822Token token : tokens) {
2054 list.append(token + END_TOKEN);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002055 }
2056 }
2057
2058 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07002059 protected Collection<String> initToRecipients(final String fullSenderAddress,
Tony Mantler24f116f2014-01-16 10:20:50 -08002060 final String[] replyToAddresses, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002061 // The To recipient is the reply-to address specified in the original
2062 // message, unless it is:
2063 // the current user OR a custom from of the current user, in which case
2064 // it's the To recipient list of the original message.
2065 // OR missing, in which case use the sender of the original message
2066 Set<String> toAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08002067 for (final String replyToAddress : replyToAddresses) {
2068 if (!TextUtils.isEmpty(replyToAddress)
2069 && !recipientMatchesThisAccount(replyToAddress)) {
2070 toAddresses.add(replyToAddress);
2071 }
2072 }
2073 if (toAddresses.size() == 0) {
mindyp65b06f52012-11-21 10:35:08 -08002074 // In this case, the user is replying to a message in which their
Tony Mantler24f116f2014-01-16 10:20:50 -08002075 // current account or some of their custom from addresses are the only
2076 // recipients and they sent the original message.
mindyp65b06f52012-11-21 10:35:08 -08002077 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
2078 && recipientMatchesThisAccount(inToAddresses[0])) {
2079 toAddresses.add(inToAddresses[0]);
2080 return toAddresses;
2081 }
2082 // This happens if the user replies to a message they originally
2083 // wrote. In this case, "reply" really means "re-send," so we
2084 // target the original recipients. This works as expected even
2085 // if the user sent the original message to themselves.
2086 for (String address : inToAddresses) {
2087 if (!recipientMatchesThisAccount(address)) {
2088 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08002089 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07002090 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002091 }
2092 return toAddresses;
2093 }
2094
Scott Kennedyff8553f2013-04-05 20:57:44 -07002095 private void addRecipients(final Set<String> recipients, final String[] addresses) {
2096 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002097 // Do not add this account, or any of its custom from addresses, to
2098 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08002099 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08002100 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08002101 recipients.add(email.replace("\"\"", ""));
2102 }
2103 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002104 }
2105
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002106 /**
2107 * A recipient matches this account if it has the same address as the
2108 * currently selected account OR one of the custom from addresses associated
2109 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002110 * @param recipientAddress address we are comparing with the currently selected account
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002111 */
mindyp5ee5d692012-11-19 16:02:16 -08002112 protected boolean recipientMatchesThisAccount(String recipientAddress) {
2113 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08002114 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07002115 }
2116
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002117 /**
2118 * Returns a formatted subject string with the appropriate prefix for the action type.
2119 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
2120 */
Tony Mantlera954f992013-12-03 11:22:56 -08002121 public static String buildFormattedSubject(final Resources res, final String subject,
2122 final int action) {
2123 final String prefix;
2124 final String correctedSubject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002125 if (action == ComposeActivity.COMPOSE) {
2126 prefix = "";
2127 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002128 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002129 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002130 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002131 }
2132
2133 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07002134 if (!TextUtils.isEmpty(subject)
2135 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002136 correctedSubject = subject;
2137 } else {
Tony Mantlera954f992013-12-03 11:22:56 -08002138 final String subjectOrNoSubject = TextUtils.isEmpty(subject) ?
2139 res.getString(R.string.no_subject) :
2140 subject;
2141
2142 correctedSubject =
2143 res.getString(R.string.formatted_subject, prefix, subjectOrNoSubject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002144 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002145
2146 return correctedSubject;
2147 }
2148
2149 private void setSubject(Message refMessage, int action) {
2150 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002151 }
2152
Mindy Pereira818143e2012-01-11 13:59:49 -08002153 private void initRecipients() {
2154 setupRecipients(mTo);
2155 setupRecipients(mCc);
2156 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002157 }
2158
Mindy Pereira818143e2012-01-11 13:59:49 -08002159 private void setupRecipients(RecipientEditTextView view) {
Andrew Sapperstein47c30c22014-06-04 10:26:22 -07002160 // todo - remove this experiment
2161 if (LogUtils.isLoggable("NewChips", LogUtils.DEBUG) || mUseNewChips) {
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002162 final DropdownChipLayouter layouter = getDropdownChipLayouter();
2163 if (layouter != null) {
2164 view.setDropdownChipLayouter(layouter);
2165 }
2166 view.setAdapter(getRecipientAdapter());
2167 } else {
2168 view.setAdapter(new RecipientAdapter(this, mAccount));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002169 }
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002170 view.setRecipientEntryItemClickedListener(this);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002171 if (mValidator == null) {
Tony Mantler79b11562013-10-09 15:31:50 -07002172 final String accountName = mAccount.getEmailAddress();
Mindy Pereira33fe9082012-01-09 16:24:30 -08002173 int offset = accountName.indexOf("@") + 1;
2174 String account = accountName;
Tony Mantler79b11562013-10-09 15:31:50 -07002175 if (offset > 0) {
2176 account = account.substring(offset);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002177 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002178 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08002179 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002180 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002181 }
2182
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002183 /**
2184 * Derived classes should override if they wish to provide their own autocomplete behavior.
2185 */
2186 public BaseRecipientAdapter getRecipientAdapter() {
2187 return new RecipientAdapter(this, mAccount);
2188 }
2189
2190 /**
2191 * Derived classes should override this to provide their own dropdown behavior.
2192 * If the result is null, the default {@link com.android.ex.chips.DropdownChipLayouter}
2193 * is used.
2194 */
2195 public DropdownChipLayouter getDropdownChipLayouter() {
2196 return null;
2197 }
2198
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002199 @Override
2200 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002201 final int id = v.getId();
2202 if (id == R.id.add_cc_bcc) {
2203 // Verify that cc/ bcc aren't showing.
2204 // Animate in cc/bcc.
2205 showCcBccViews();
Andrew Sapperstein6aea7862013-10-24 19:59:51 -07002206 } else if (id == R.id.add_attachment) {
2207 doAttach(Utils.isRunningKitkatOrLater() ? MIME_TYPE_ALL : MIME_TYPE_PHOTO);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002208 }
2209 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002210
2211 @Override
2212 public boolean onCreateOptionsMenu(Menu menu) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002213 final boolean superCreated = super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07002214 // Don't render any menu items when there are no accounts.
2215 if (mAccounts == null || mAccounts.length == 0) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07002216 return superCreated;
Mindy Pereirab199d172012-08-13 11:04:03 -07002217 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002218 MenuInflater inflater = getMenuInflater();
2219 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08002220
2221 /*
2222 * Start save in the correct enabled state.
2223 * 1) If a user launches compose from within gmail, save is disabled
2224 * until they add something, at which point, save is enabled, auto save
2225 * on exit; if the user empties everything, save is disabled, exiting does not
2226 * auto-save
2227 * 2) if a user replies/ reply all/ forwards from within gmail, save is
2228 * disabled until they change something, at which point, save is
2229 * enabled, auto save on exit; if the user empties everything, save is
2230 * disabled, exiting does not auto-save.
2231 * 3) If a user launches compose from another application and something
2232 * gets populated (attachments, recipients, body, subject, etc), save is
2233 * enabled, auto save on exit; if the user empties everything, save is
2234 * disabled, exiting does not auto-save
2235 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08002236 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08002237 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07002238 enableSave(mInnerSavedState != null ?
2239 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08002240 : (Intent.ACTION_SEND.equals(action)
2241 || Intent.ACTION_SEND_MULTIPLE.equals(action)
2242 || Intent.ACTION_SENDTO.equals(action)
2243 || shouldSave()));
2244
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07002245 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2246 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2247 if (helpItem != null) {
2248 helpItem.setVisible(mAccount != null
2249 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2250 }
2251 if (sendFeedbackItem != null) {
2252 sendFeedbackItem.setVisible(mAccount != null
2253 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2254 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002255
Andrew Sapperstein8809f9f2013-10-11 16:13:35 -07002256 // Show attach picture on pre-K devices.
2257 menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002258
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002259 return true;
2260 }
2261
2262 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002263 public boolean onPrepareOptionsMenu(Menu menu) {
2264 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08002265 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002266 // Its possible there is a menu item OR a button.
2267 boolean ccFieldVisible = mCc.isShown();
2268 boolean bccFieldVisible = mBcc.isShown();
2269 if (!ccFieldVisible || !bccFieldVisible) {
2270 ccBcc.setVisible(true);
2271 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
2272 : R.string.add_bcc_label));
2273 } else {
2274 ccBcc.setVisible(false);
2275 }
2276 }
2277 return true;
2278 }
2279
2280 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002281 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002282 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002283
Andy Huangf8c59b02014-03-19 20:00:53 -07002284 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id,
2285 "compose", 0);
Andy Huangdc97bf42013-08-15 16:52:45 -07002286
Mindy Pereira75f66632012-01-11 11:42:02 -08002287 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002288 if (id == R.id.add_file_attachment) {
2289 doAttach(MIME_TYPE_ALL);
2290 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002291 doAttach(MIME_TYPE_PHOTO);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002292 } else if (id == R.id.add_cc_bcc) {
2293 showCcBccViews();
2294 } else if (id == R.id.save) {
2295 doSave(true);
2296 } else if (id == R.id.send) {
2297 doSend();
2298 } else if (id == R.id.discard) {
2299 doDiscard();
2300 } else if (id == R.id.settings) {
2301 Utils.showSettings(this, mAccount);
2302 } else if (id == android.R.id.home) {
2303 onAppUpPressed();
2304 } else if (id == R.id.help_info_menu_item) {
2305 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2306 } else if (id == R.id.feedback_menu_item) {
2307 Utils.sendFeedback(this, mAccount, false);
2308 } else {
2309 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002310 }
Tony Mantler581edd42014-02-18 15:41:22 -08002311 return handled || super.onOptionsItemSelected(item);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002312 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002313
Mindy Pereirab199d172012-08-13 11:04:03 -07002314 @Override
2315 public void onBackPressed() {
2316 // If we are showing the wait fragment, just exit.
2317 if (getWaitFragment() != null) {
2318 finish();
2319 } else {
2320 super.onBackPressed();
2321 }
2322 }
2323
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002324 /**
2325 * Carries out the "up" action in the action bar.
2326 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002327 private void onAppUpPressed() {
2328 if (mLaunchedFromEmail) {
2329 // If this was started from Gmail, simply treat app up as the system back button, so
2330 // that the last view is restored.
2331 onBackPressed();
2332 return;
2333 }
2334
2335 // Fire the main activity to ensure it launches the "top" screen of mail.
2336 // Since the main Activity is singleTask, it should revive that task if it was already
2337 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002338 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002339 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2340 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2341 startActivity(mailIntent);
2342 finish();
2343 }
2344
Mindy Pereira33fe9082012-01-09 16:24:30 -08002345 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002346 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002347 logSendOrSave(false /* save */);
2348 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002349 }
2350
Mindy Pereira48e31b02012-05-30 13:12:24 -07002351 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002352 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002353 }
2354
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002355 @Override
2356 public void onRecipientEntryItemClicked(int charactersTyped, int position) {
2357 // Send analytics of characters typed and position in dropdown selected.
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002358 final String category = mUseNewChips ? "suggest_click_new" : "suggest_click_old";
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002359 Analytics.getInstance().sendEvent(
Andrew Sapperstein50453e42014-05-16 09:25:10 -07002360 category, Integer.toString(charactersTyped), Integer.toString(position), 0);
Andrew Sappersteinffd61552014-05-14 15:04:23 -07002361 }
2362
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002363 @VisibleForTesting
2364 public interface SendOrSaveCallback {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002365 void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
2366 void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2367 Message getMessage();
2368 void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2369 void incrementRecipientsTimesContacted(List<String> recipients);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002370 }
2371
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002372 @VisibleForTesting
2373 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002374 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002375 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002376 public final SendOrSaveCallback mSendOrSaveCallback;
2377 @VisibleForTesting
2378 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002379 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002380
2381 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002382 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002383 mContext = context;
2384 mSendOrSaveCallback = callback;
2385 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002386 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002387 }
2388
2389 @Override
2390 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002391 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002392
Mindy Pereira92551d02012-04-05 11:31:12 -07002393 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002394 Message message = mSendOrSaveCallback.getMessage();
2395 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002396 // If a previous draft has been saved, in an account that is different
2397 // than what the user wants to send from, remove the old draft, and treat this
2398 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002399 if (mExistingDraftAccount != null
2400 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002401 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2402 ContentResolver resolver = mContext.getContentResolver();
2403 ContentValues values = new ContentValues();
2404 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002405 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2406 new ContentProviderTask.UpdateTask()
2407 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2408 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002409 } else {
2410 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002411 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002412 // reset messageId to 0, so a new message will be created
2413 messageId = UIProvider.INVALID_MESSAGE_ID;
2414 }
2415 }
2416
2417 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002418 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002419
2420 if (!sendOrSaveMessage.mSave) {
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002421 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002422 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002423 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002424 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002425 incrementRecipientsTimesContacted(
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002426 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2427 }
2428 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2429 }
2430
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002431 private void incrementRecipientsTimesContacted(final String addressString) {
Tony Mantler9f324232013-08-08 14:24:30 -07002432 if (TextUtils.isEmpty(addressString)) {
2433 return;
2434 }
2435 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2436 final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
Tony Mantler581edd42014-02-18 15:41:22 -08002437 for (final Rfc822Token token : tokens) {
2438 recipients.add(token.getAddress());
Tony Mantler9f324232013-08-08 14:24:30 -07002439 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002440 mSendOrSaveCallback.incrementRecipientsTimesContacted(recipients);
Tony Mantler9f324232013-08-08 14:24:30 -07002441 }
2442
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002443 /**
2444 * Send or Save a message.
2445 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002446 private void sendOrSaveMessage(final long messageIdToSave,
2447 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002448 final ContentResolver resolver = mContext.getContentResolver();
2449 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2450
2451 final String accountMethod = sendOrSaveMessage.mSave ?
2452 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2453 UIProvider.AccountCallMethods.SEND_MESSAGE;
2454
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002455 try {
2456 if (updateExistingMessage) {
2457 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002458
Paul Westbrook013a23c2013-02-22 10:37:41 -08002459 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002460 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002461 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002462 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002463 final Bundle result = callAccountSendSaveMethod(resolver,
2464 selectedAccount.account, accountMethod, sendOrSaveMessage);
2465 if (result != null) {
2466 // If a non-null value was returned, then the provider handled the call
2467 // method
2468 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002469 }
2470 if (sendOrSaveMessage.mSave && messageUri != null) {
2471 final Cursor messageCursor = resolver.query(messageUri,
2472 UIProvider.MESSAGE_PROJECTION, null, null, null);
2473 if (messageCursor != null) {
2474 try {
2475 if (messageCursor.moveToFirst()) {
2476 // Broadcast notification that a new message has
2477 // been allocated
2478 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2479 new Message(messageCursor));
2480 }
2481 } finally {
2482 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002483 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002484 }
2485 }
2486 }
2487 } finally {
2488 // Close any opened file descriptors
2489 closeOpenedAttachmentFds(sendOrSaveMessage);
2490 }
2491 }
2492
Scott Kennedyff8553f2013-04-05 20:57:44 -07002493 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002494 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2495 if (openedFds != null) {
2496 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002497 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002498 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2499 if (fd != null) {
2500 try {
2501 fd.close();
2502 } catch (IOException e) {
2503 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002504 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002505 }
2506 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002507 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002508 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002509
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002510 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002511 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002512 *
2513 * If this was successful, this method will return an non-null Bundle instance
2514 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002515 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2516 final Account account, final String method,
2517 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002518 // Copy all of the values from the content values to the bundle
2519 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2520 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2521
2522 for (Entry<String, Object> entry : valueSet) {
2523 final Object entryValue = entry.getValue();
2524 final String key = entry.getKey();
2525 if (entryValue instanceof String) {
2526 methodExtras.putString(key, (String)entryValue);
2527 } else if (entryValue instanceof Boolean) {
2528 methodExtras.putBoolean(key, (Boolean)entryValue);
2529 } else if (entryValue instanceof Integer) {
2530 methodExtras.putInt(key, (Integer)entryValue);
2531 } else if (entryValue instanceof Long) {
2532 methodExtras.putLong(key, (Long)entryValue);
2533 } else {
2534 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2535 entryValue.getClass().getName());
2536 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002537 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002538
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002539 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2540 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2541 if (fdMap != null) {
2542 methodExtras.putParcelable(
2543 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2544 }
2545
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002546 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002547 }
2548 }
2549
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07002550 /**
2551 * Reports recipients that have been contacted in order to improve auto-complete
2552 * suggestions. Default behavior updates usage statistics in ContactsProvider.
2553 * @param recipients addresses
2554 */
2555 protected void incrementRecipientsTimesContacted(List<String> recipients) {
2556 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(this);
2557 statsUpdater.updateWithAddress(recipients);
2558 }
2559
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002560 @VisibleForTesting
2561 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002562 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002563 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002564 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002565 @VisibleForTesting
2566 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002567 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002568 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002569
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002570 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2571 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002572 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002573 mValues = values;
2574 mRefMessageId = refMessageId;
2575 mSave = save;
2576 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002577
2578 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002579 }
2580
2581 int requestId() {
2582 return mRequestId;
2583 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002584
2585 Bundle attachmentFds() {
2586 return mAttachmentFds;
2587 }
2588
2589 /**
2590 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2591 * called before the ComposeActivity finishes.
2592 * Note: The caller is responsible for closing these file descriptors.
2593 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002594 private static Bundle initializeAttachmentFds(final Context context,
2595 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002596 if (attachments == null || attachments.size() == 0) {
2597 return null;
2598 }
2599
2600 final Bundle result = new Bundle(attachments.size());
2601 final ContentResolver resolver = context.getContentResolver();
2602
2603 for (Attachment attachment : attachments) {
2604 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2605 continue;
2606 }
2607
2608 ParcelFileDescriptor fileDescriptor;
2609 try {
2610 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2611 } catch (FileNotFoundException e) {
2612 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2613 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002614 } catch (SecurityException e) {
2615 // We have encountered a security exception when attempting to open the file
2616 // specified by the content uri. If the attachment has been cached, this
2617 // isn't a problem, as even through the original permission may have been
2618 // revoked, we have cached the file. This will happen when saving/sending
2619 // a previously saved draft.
2620 // TODO(markwei): Expose whether the attachment has been cached through the
2621 // attachment object. This would allow us to limit when the log is made, as
2622 // if the attachment has been cached, this really isn't an error
2623 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2624 // Just set the file descriptor to null, as the underlying provider needs
2625 // to handle the file descriptor not being set.
2626 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002627 }
2628
2629 if (fileDescriptor != null) {
2630 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2631 }
2632 }
2633
2634 return result;
2635 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002636 }
2637
2638 /**
2639 * Get the to recipients.
2640 */
2641 public String[] getToAddresses() {
2642 return getAddressesFromList(mTo);
2643 }
2644
2645 /**
2646 * Get the cc recipients.
2647 */
2648 public String[] getCcAddresses() {
2649 return getAddressesFromList(mCc);
2650 }
2651
2652 /**
2653 * Get the bcc recipients.
2654 */
2655 public String[] getBccAddresses() {
2656 return getAddressesFromList(mBcc);
2657 }
2658
2659 public String[] getAddressesFromList(RecipientEditTextView list) {
2660 if (list == null) {
2661 return new String[0];
2662 }
2663 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2664 int count = tokens.length;
2665 String[] result = new String[count];
2666 for (int i = 0; i < count; i++) {
2667 result[i] = tokens[i].toString();
2668 }
2669 return result;
2670 }
2671
2672 /**
2673 * Check for invalid email addresses.
2674 * @param to String array of email addresses to check.
2675 * @param wrongEmailsOut Emails addresses that were invalid.
2676 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002677 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002678 if (mValidator == null) {
2679 return;
2680 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002681 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002682 if (!mValidator.isValid(email)) {
2683 wrongEmailsOut.add(email);
2684 }
2685 }
2686 }
2687
Tony Mantler2558b502013-07-09 10:53:34 -07002688 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002689 // Public no-args constructor needed for fragment re-instantiation
2690 public RecipientErrorDialogFragment() {}
2691
Tony Mantler2558b502013-07-09 10:53:34 -07002692 public static RecipientErrorDialogFragment newInstance(final String message) {
2693 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2694 final Bundle args = new Bundle(1);
2695 args.putString("message", message);
2696 frag.setArguments(args);
2697 return frag;
2698 }
2699
2700 @Override
2701 public Dialog onCreateDialog(Bundle savedInstanceState) {
2702 final String message = getArguments().getString("message");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002703 return new AlertDialog.Builder(getActivity())
2704 .setMessage(message)
Tony Mantler2558b502013-07-09 10:53:34 -07002705 .setPositiveButton(
2706 R.string.ok, new Dialog.OnClickListener() {
2707 @Override
2708 public void onClick(DialogInterface dialog, int which) {
2709 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2710 }
2711 }).create();
2712 }
2713 }
2714
2715 private void finishRecipientErrorDialog() {
2716 // after the user dismisses the recipient error
2717 // dialog we want to make sure to refocus the
2718 // recipient to field so they can fix the issue
2719 // easily
2720 if (mTo != null) {
2721 mTo.requestFocus();
2722 }
2723 }
2724
Mindy Pereira82cc5662012-01-09 17:29:30 -08002725 /**
2726 * Show an error because the user has entered an invalid recipient.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002727 */
Tony Mantler2558b502013-07-09 10:53:34 -07002728 private void showRecipientErrorDialog(final String message) {
2729 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2730 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002731 }
2732
2733 /**
2734 * Update the state of the UI based on whether or not the current draft
2735 * needs to be saved and the message is not empty.
2736 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002737 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002738 if (mSave != null) {
2739 mSave.setEnabled((shouldSave() && !isBlank()));
2740 }
2741 }
2742
2743 /**
2744 * Returns true if we need to save the current draft.
2745 */
2746 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002747 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002748 // The message should only be saved if:
2749 // It hasn't been sent AND
2750 // Some text has been added to the message OR
2751 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002752 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002753 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002754 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002755 }
2756 }
2757
2758 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002759 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002760 * @return boolean
2761 */
2762 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002763 // Need to check for null since isBlank() can be called from onPause()
2764 // before findViews() is called
2765 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2766 mAttachmentsView == null) {
2767 LogUtils.w(LOG_TAG, "null views in isBlank check");
2768 return true;
2769 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002770 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002771 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2772 mBodyView.getText().toString()) == 0)
2773 && mTo.length() == 0
2774 && mCc.length() == 0 && mBcc.length() == 0
2775 && mAttachmentsView.getAttachments().size() == 0;
2776 }
2777
2778 @VisibleForTesting
2779 protected int getSignatureStartPosition(String signature, String bodyText) {
2780 int startPos = -1;
2781
2782 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2783 return startPos;
2784 }
2785
2786 int bodyLength = bodyText.length();
2787 int signatureLength = signature.length();
2788 String printableVersion = convertToPrintableSignature(signature);
2789 int printableLength = printableVersion.length();
2790
2791 if (bodyLength >= printableLength
2792 && bodyText.substring(bodyLength - printableLength)
2793 .equals(printableVersion)) {
2794 startPos = bodyLength - printableLength;
2795 } else if (bodyLength >= signatureLength
2796 && bodyText.substring(bodyLength - signatureLength)
2797 .equals(signature)) {
2798 startPos = bodyLength - signatureLength;
2799 }
2800 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002801 }
2802
2803 /**
2804 * Allows any changes made by the user to be ignored. Called when the user
2805 * decides to discard a draft.
2806 */
2807 private void discardChanges() {
2808 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002809 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002810 mReplyFromChanged = false;
2811 }
2812
2813 /**
Tony Mantler581edd42014-02-18 15:41:22 -08002814 * @param save True to save, false to send
2815 * @param showToast True to show a toast once the message is sent/saved
Mindy Pereira181df782012-03-01 13:32:44 -08002816 */
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002817 protected void sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002818 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002819 if (mAccounts == null || mAccount == null) {
2820 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002821 if (autoSend) {
2822 finish();
2823 }
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002824 return;
Mark Wei009b3712012-10-18 18:07:50 -07002825 }
2826
Scott Kennedyff8553f2013-04-05 20:57:44 -07002827 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002828 if (orientationChanged) {
2829 to = cc = bcc = new String[0];
2830 } else {
2831 to = getToAddresses();
2832 cc = getCcAddresses();
2833 bcc = getBccAddresses();
2834 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002835
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002836 final ArrayList<String> recipients = buildEmailAddressList(to);
2837 recipients.addAll(buildEmailAddressList(cc));
2838 recipients.addAll(buildEmailAddressList(bcc));
2839
Mindy Pereira181df782012-03-01 13:32:44 -08002840 // Don't let the user send to nobody (but it's okay to save a message
2841 // with no recipients)
2842 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2843 showRecipientErrorDialog(getString(R.string.recipient_needed));
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002844 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002845 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002846
Mindy Pereira181df782012-03-01 13:32:44 -08002847 List<String> wrongEmails = new ArrayList<String>();
2848 if (!save) {
2849 checkInvalidEmails(to, wrongEmails);
2850 checkInvalidEmails(cc, wrongEmails);
2851 checkInvalidEmails(bcc, wrongEmails);
2852 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002853
Mindy Pereira181df782012-03-01 13:32:44 -08002854 // Don't let the user send an email with invalid recipients
2855 if (wrongEmails.size() > 0) {
2856 String errorText = String.format(getString(R.string.invalid_recipient),
2857 wrongEmails.get(0));
2858 showRecipientErrorDialog(errorText);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002859 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002860 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002861
Mindy Pereira181df782012-03-01 13:32:44 -08002862 if (!save) {
Alan Lau3d519042014-06-05 11:13:06 -07002863 if (autoSend) {
2864 // Skip all further checks during autosend. This flow is used by Android Wear
2865 // and Google Now.
2866 sendOrSave(save, showToast);
2867 return;
2868 }
2869
2870 // Show a warning before sending only if there are no attachments, body, or subject.
Mindy Pereira181df782012-03-01 13:32:44 -08002871 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2872 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002873 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002874
Mindy Pereira181df782012-03-01 13:32:44 -08002875 // A warning about an empty body may not be warranted when
2876 // forwarding mails, since a common use case is to forward
2877 // quoted text and not append any more text.
2878 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002879
Mindy Pereira181df782012-03-01 13:32:44 -08002880 // When we bring up a dialog warning the user about a send,
2881 // assume that they accept sending the message. If they do not,
2882 // the dialog listener is required to enable sending again.
2883 if (warnAboutEmptySubject) {
Tony Mantler581edd42014-02-18 15:41:22 -08002884 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002885 showToast, recipients);
2886 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002887 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002888
Mindy Pereira181df782012-03-01 13:32:44 -08002889 if (warnAboutEmptyBody) {
Tony Mantler581edd42014-02-18 15:41:22 -08002890 showSendConfirmDialog(R.string.confirm_send_message_with_no_body,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002891 showToast, recipients);
2892 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002893 }
2894 }
Alan Lau3d519042014-06-05 11:13:06 -07002895 // Ask for confirmation to send.
2896 if (showSendConfirmation()) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002897 showSendConfirmDialog(R.string.confirm_send_message, showToast, recipients);
2898 return;
Mindy Pereira181df782012-03-01 13:32:44 -08002899 }
2900 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002901
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002902 performAdditionalSendOrSaveSanityChecks(save, showToast, recipients);
Mindy Pereira181df782012-03-01 13:32:44 -08002903 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002904
Mindy Pereira181df782012-03-01 13:32:44 -08002905 /**
2906 * Returns a boolean indicating whether warnings should be shown for empty
2907 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002908 *
Mindy Pereira181df782012-03-01 13:32:44 -08002909 * @return True if a warning should be shown for empty text fields
2910 */
2911 protected boolean showEmptyTextWarnings() {
2912 return mAttachmentsView.getAttachments().size() == 0;
2913 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002914
Mindy Pereira181df782012-03-01 13:32:44 -08002915 /**
2916 * Returns a boolean indicating whether the user should confirm each send
2917 *
2918 * @return True if a warning should be on each send
2919 */
2920 protected boolean showSendConfirmation() {
Tony Mantler581edd42014-02-18 15:41:22 -08002921 return mCachedSettings != null && mCachedSettings.confirmSend;
Mindy Pereira181df782012-03-01 13:32:44 -08002922 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002923
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002924 public static class SendConfirmDialogFragment extends DialogFragment
2925 implements DialogInterface.OnClickListener {
2926
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002927 private static final String MESSAGE_ID = "messageId";
2928 private static final String SHOW_TOAST = "showToast";
2929 private static final String RECIPIENTS = "recipients";
2930
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002931 private boolean mShowToast;
2932
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002933 private ArrayList<String> mRecipients;
2934
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002935 // Public no-args constructor needed for fragment re-instantiation
2936 public SendConfirmDialogFragment() {}
2937
Tony Mantler2558b502013-07-09 10:53:34 -07002938 public static SendConfirmDialogFragment newInstance(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002939 final boolean showToast, final ArrayList<String> recipients) {
Tony Mantler2558b502013-07-09 10:53:34 -07002940 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2941 final Bundle args = new Bundle(3);
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002942 args.putInt(MESSAGE_ID, messageId);
2943 args.putBoolean(SHOW_TOAST, showToast);
2944 args.putStringArrayList(RECIPIENTS, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07002945 frag.setArguments(args);
2946 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002947 }
Tony Mantler2558b502013-07-09 10:53:34 -07002948
2949 @Override
2950 public Dialog onCreateDialog(Bundle savedInstanceState) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002951 final int messageId = getArguments().getInt(MESSAGE_ID);
2952 mShowToast = getArguments().getBoolean(SHOW_TOAST);
2953 mRecipients = getArguments().getStringArrayList(RECIPIENTS);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002954
2955 final int confirmTextId = (messageId == R.string.confirm_send_message) ?
2956 R.string.ok : R.string.send;
Tony Mantler2558b502013-07-09 10:53:34 -07002957
2958 return new AlertDialog.Builder(getActivity())
2959 .setMessage(messageId)
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002960 .setPositiveButton(confirmTextId, this)
Paul Westbrook7d1c5c42013-10-01 23:40:04 -07002961 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07002962 .create();
2963 }
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002964
2965 @Override
2966 public void onClick(DialogInterface dialog, int which) {
2967 if (which == DialogInterface.BUTTON_POSITIVE) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002968 ((ComposeActivity) getActivity()).finishSendConfirmDialog(mShowToast, mRecipients);
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002969 }
2970 }
Tony Mantler2558b502013-07-09 10:53:34 -07002971 }
2972
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002973 private void finishSendConfirmDialog(
2974 final boolean showToast, final ArrayList<String> recipients) {
2975 performAdditionalSendOrSaveSanityChecks(false /* save */, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07002976 }
2977
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002978 // The list of recipients are used by the additional sendOrSave checks.
2979 // However, the send confirm dialog may be shown before performing
2980 // the additional checks. As a result, we need to plumb the recipient
2981 // list through the send confirm dialog so that
2982 // performAdditionalSendOrSaveChecks can be performed properly.
Tony Mantler581edd42014-02-18 15:41:22 -08002983 private void showSendConfirmDialog(final int messageId,
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07002984 final boolean showToast, final ArrayList<String> recipients) {
2985 final DialogFragment frag = SendConfirmDialogFragment.newInstance(
2986 messageId, showToast, recipients);
Tony Mantler2558b502013-07-09 10:53:34 -07002987 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08002988 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002989
Mindy Pereira181df782012-03-01 13:32:44 -08002990 /**
2991 * Returns whether the ComposeArea believes there is any text in the body of
2992 * the composition. TODO: When ComposeArea controls the Body as well, add
2993 * that here.
2994 */
2995 public boolean isBodyEmpty() {
2996 return !mQuotedTextView.isTextIncluded();
2997 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002998
Mindy Pereira181df782012-03-01 13:32:44 -08002999 /**
3000 * Test to see if the subject is empty.
3001 *
3002 * @return boolean.
3003 */
3004 // TODO: this will likely go away when composeArea.focus() is implemented
3005 // after all the widget control is moved over.
3006 public boolean isSubjectEmpty() {
3007 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
3008 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003009
Andy Huang0a2a3462013-12-20 15:56:13 -08003010 @VisibleForTesting
3011 public String getSubject() {
3012 return mSubject.getText().toString();
3013 }
3014
Andy Huang91ede362014-01-21 19:16:00 -08003015 private int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07003016 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08003017 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07003018 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08003019 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003020
Paul Westbrookb4931c62013-01-14 17:51:18 -08003021 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07003022
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003023 MessageModification.putToAddresses(values, message.getToAddresses());
3024 MessageModification.putCcAddresses(values, message.getCcAddresses());
3025 MessageModification.putBccAddresses(values, message.getBccAddresses());
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003026 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07003027
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003028 MessageModification.putSubject(values, message.subject);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003029
Paul Westbrookb4931c62013-01-14 17:51:18 -08003030 // Make sure to remove only the composing spans from the Spannable before saving.
Andy Huang91ede362014-01-21 19:16:00 -08003031 final String htmlBody = spannedBodyToHtml(body);
Anthony Lee2a3cc132014-04-22 14:15:25 -07003032 final String textBody = Utils.convertHtmlToPlainText(htmlBody);
3033 // fullbody will contain the actual body plus the quoted text.
3034 final String fullBody;
3035 final String quotedString;
3036 final boolean hasQuotedText = !TextUtils.isEmpty(quotedText);
3037 if (hasQuotedText) {
3038 // The quoted text is HTML at this point.
3039 quotedString = quotedText.toString();
3040 fullBody = htmlBody + quotedString;
3041 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
3042 MessageModification.putAppendRefMessageContent(values, true /* include quoted */);
3043 } else {
3044 fullBody = htmlBody;
3045 quotedString = null;
Mindy Pereira29ef1b82012-01-13 11:26:21 -08003046 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003047 if (refMessage != null) {
Anthony Lee2a3cc132014-04-22 14:15:25 -07003048 // The code below might need to be revisited. The quoted text position is different
3049 // between text/html and text/plain parts and they should be stored seperately and
3050 // the right version should be used in the UI. text/html should have preference
3051 // if both exist. Issues like this made me file b/14256940 to make sure that we
3052 // properly handle the existing of both text/html and text/plain parts and to verify
3053 // that we are not making some assumptions that break if there is no text/html part.
3054 int quotedTextPos = -1;
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003055 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
3056 MessageModification.putBodyHtml(values, fullBody.toString());
Anthony Lee2a3cc132014-04-22 14:15:25 -07003057 if (hasQuotedText) {
3058 quotedTextPos = htmlBody.length() +
3059 QuotedTextView.getQuotedTextOffset(quotedString);
3060 }
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003061 }
3062 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08003063 MessageModification.putBody(values,
Tony Mantler581edd42014-02-18 15:41:22 -08003064 Utils.convertHtmlToPlainText(fullBody.toString()));
Anthony Lee2a3cc132014-04-22 14:15:25 -07003065 if (hasQuotedText && (quotedTextPos == -1)) {
3066 quotedTextPos = textBody.length();
3067 }
3068 }
3069 if (quotedTextPos != -1) {
3070 // The quoted text pos is the text/html version first and the text/plan version
3071 // if there is no text/html part. The reason for this is because preference
3072 // is given to text/html in the compose window if it exists. In the future, we
3073 // should calculate the index for both since the user could choose to compose
3074 // explicitly in text/plain.
3075 MessageModification.putQuoteStartPos(values, quotedTextPos);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07003076 }
3077 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07003078 MessageModification.putBodyHtml(values, fullBody.toString());
Tony Mantler581edd42014-02-18 15:41:22 -08003079 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString()));
Mindy Pereirac2031972012-04-03 09:38:35 -07003080 }
Anthony Lee2a3cc132014-04-22 14:15:25 -07003081 int draftType = getDraftType(composeMode);
3082 MessageModification.putDraftType(values, draftType);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07003083 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07003084 if (!TextUtils.isEmpty(refMessageId)) {
3085 MessageModification.putRefMessageId(values, refMessageId);
3086 }
Scott Kennedy60847252013-08-15 15:55:42 -07003087 if (extraValues != null) {
3088 values.putAll(extraValues);
3089 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07003090 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
3091 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08003092 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
3093 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003094
Mindy Pereira181df782012-03-01 13:32:44 -08003095 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08003096 // Do the send/save action on the specified handler to avoid possible
3097 // ANRs
3098 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003099
Mindy Pereira181df782012-03-01 13:32:44 -08003100 return sendOrSaveMessage.requestId();
3101 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003102
Paul Westbrookb4931c62013-01-14 17:51:18 -08003103 /**
3104 * Removes any composing spans from the specified string. This will create a new
3105 * SpannableString instance, as to not modify the behavior of the EditText view.
3106 */
3107 private static SpannableString removeComposingSpans(Spanned body) {
3108 final SpannableString messageBody = new SpannableString(body);
3109 BaseInputConnection.removeComposingSpans(messageBody);
3110 return messageBody;
3111 }
3112
Mindy Pereira002ff522012-05-30 10:31:26 -07003113 private static int getDraftType(int mode) {
3114 int draftType = -1;
3115 switch (mode) {
3116 case ComposeActivity.COMPOSE:
3117 draftType = DraftType.COMPOSE;
3118 break;
3119 case ComposeActivity.REPLY:
3120 draftType = DraftType.REPLY;
3121 break;
3122 case ComposeActivity.REPLY_ALL:
3123 draftType = DraftType.REPLY_ALL;
3124 break;
3125 case ComposeActivity.FORWARD:
3126 draftType = DraftType.FORWARD;
3127 break;
3128 }
3129 return draftType;
3130 }
3131
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003132 /**
3133 * Derived classes should override this step to perform additional checks before
3134 * send or save. The default implementation simply calls {@link #sendOrSave(boolean, boolean)}.
3135 */
3136 protected void performAdditionalSendOrSaveSanityChecks(
3137 final boolean save, final boolean showToast, ArrayList<String> recipients) {
3138 sendOrSave(save, showToast);
3139 }
3140
3141 protected void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08003142 // Check if user is a monkey. Monkeys can compose and hit send
3143 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07003144 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08003145 return;
3146 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003147
Tony Mantler2558b502013-07-09 10:53:34 -07003148 final Spanned body = mBodyView.getEditableText();
3149
Mindy Pereira181df782012-03-01 13:32:44 -08003150 SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003151 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08003152 private int mRestoredRequestId;
3153
Marc Blank0bbc8582012-04-23 15:07:57 -07003154 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08003155 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08003156 synchronized (mActiveTasks) {
3157 int numTasks = mActiveTasks.size();
3158 if (numTasks == 0) {
3159 // Start service so we won't be killed if this app is
3160 // put in the background.
3161 startService(new Intent(ComposeActivity.this, EmptyService.class));
3162 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003163
Mindy Pereira181df782012-03-01 13:32:44 -08003164 mActiveTasks.add(sendOrSaveTask);
3165 }
3166 if (sTestSendOrSaveCallback != null) {
3167 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
3168 }
3169 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003170
Marc Blank0bbc8582012-04-23 15:07:57 -07003171 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003172 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
3173 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08003174 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08003175 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08003176 mDraftId = message.id;
3177 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003178 if (sRequestMessageIdMap != null) {
3179 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
3180 }
Mindy Pereira181df782012-03-01 13:32:44 -08003181 // Cache request message map, in case the process is killed
3182 saveRequestMap();
3183 }
3184 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003185 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08003186 }
3187 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003188
Marc Blank0bbc8582012-04-23 15:07:57 -07003189 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003190 public Message getMessage() {
3191 synchronized (mDraftLock) {
3192 return mDraft;
3193 }
3194 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003195
Marc Blank0bbc8582012-04-23 15:07:57 -07003196 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003197 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07003198 // Update the last sent from account.
3199 if (mAccount != null) {
3200 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
3201 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003202 if (success) {
3203 // Successfully sent or saved so reset change markers
3204 discardChanges();
3205 } else {
3206 // A failure happened with saving/sending the draft
3207 // TODO(pwestbro): add a better string that should be used
3208 // when failing to send or save
3209 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
3210 .show();
3211 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003212
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003213 int numTasks;
3214 synchronized (mActiveTasks) {
3215 // Remove the task from the list of active tasks
3216 mActiveTasks.remove(task);
3217 numTasks = mActiveTasks.size();
3218 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003219
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003220 if (numTasks == 0) {
3221 // Stop service so we can be killed.
3222 stopService(new Intent(ComposeActivity.this, EmptyService.class));
3223 }
3224 if (sTestSendOrSaveCallback != null) {
3225 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
3226 }
3227 }
Andrew Sappersteinf5ab8ac2014-05-02 15:14:54 -07003228
3229 @Override
3230 public void incrementRecipientsTimesContacted(final List<String> recipients) {
3231 ComposeActivity.this.incrementRecipientsTimesContacted(recipients);
3232 }
Mindy Pereira181df782012-03-01 13:32:44 -08003233 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08003234
Tony Mantler1e05a1e2013-08-12 16:44:26 -07003235 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003236
Anthony Lee2a3cc132014-04-22 14:15:25 -07003237 Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07003238 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
3239 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Jin Cao5134be52014-05-06 19:18:38 -07003240 SEND_SAVE_TASK_HANDLER, save, mComposeMode, mDraftAccount, mExtraValues);
Mindy Pereira82cc5662012-01-09 17:29:30 -08003241
Mindy Pereira181df782012-03-01 13:32:44 -08003242 // Don't display the toast if the user is just changing the orientation,
3243 // but we still need to save the draft to the cursor because this is how we restore
3244 // the attachments when the configuration change completes.
3245 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
3246 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
3247 Toast.LENGTH_LONG).show();
3248 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003249
Mindy Pereira181df782012-03-01 13:32:44 -08003250 // Need to update variables here because the send or save completes
3251 // asynchronously even though the toast shows right away.
3252 discardChanges();
3253 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003254
Mindy Pereira181df782012-03-01 13:32:44 -08003255 // If we are sending, finish the activity
3256 if (!save) {
3257 finish();
3258 }
3259 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003260
Mindy Pereira181df782012-03-01 13:32:44 -08003261 /**
3262 * Save the state of the request messageid map. This allows for the Gmail
3263 * process to be killed, but and still allow for ComposeActivity instances
3264 * to be recreated correctly.
3265 */
3266 private void saveRequestMap() {
3267 // TODO: store the request map in user preferences.
3268 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003269
Tony Mantler581edd42014-02-18 15:41:22 -08003270 @SuppressLint("NewApi")
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003271 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08003272 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
3273 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07003274 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07003275 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07003276 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08003277 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08003278 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
3279 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08003280 }
3281
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003282 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003283 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003284 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07003285 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003286 }
3287 }
3288
Andy Huang4fe0af82013-08-20 17:24:51 -07003289 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003290 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07003291 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003292 case COMPOSE:
3293 msgType = "new_message";
3294 break;
3295 case REPLY:
3296 msgType = "reply";
3297 break;
3298 case REPLY_ALL:
3299 msgType = "reply_all";
3300 break;
3301 case FORWARD:
3302 msgType = "forward";
3303 break;
3304 default:
3305 msgType = "unknown";
3306 break;
3307 }
Andy Huang4fe0af82013-08-20 17:24:51 -07003308 return msgType;
3309 }
3310
3311 private void logSendOrSave(boolean save) {
3312 if (!Analytics.isLoggable() || mAttachmentsView == null) {
3313 return;
3314 }
3315
3316 final String category = (save) ? "message_save" : "message_send";
3317 final int attachmentCount = getAttachments().size();
3318 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07003319 final String label;
3320 final long value;
3321 if (mComposeMode == COMPOSE) {
3322 label = Integer.toString(attachmentCount);
3323 value = attachmentCount;
3324 } else {
3325 label = null;
3326 value = 0;
3327 }
3328 Analytics.getInstance().sendEvent(category, msgType, label, value);
3329 }
3330
Mindy Pereira326c6602012-01-04 15:32:42 -08003331 @Override
3332 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003333 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003334 if (position == ComposeActivity.REPLY) {
3335 mComposeMode = ComposeActivity.REPLY;
3336 } else if (position == ComposeActivity.REPLY_ALL) {
3337 mComposeMode = ComposeActivity.REPLY_ALL;
3338 } else if (position == ComposeActivity.FORWARD) {
3339 mComposeMode = ComposeActivity.FORWARD;
3340 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003341 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003342 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003343 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003344 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003345 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003346 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003347 boolean showCc = false;
3348 boolean showBcc = false;
3349 if (mDraft != null) {
3350 // Following desktop behavior, if the user has added a BCC
3351 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003352 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003353 // Use the draft to determine what to populate.
3354 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003355 showCc = showBcc
3356 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003357 }
3358 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003359 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003360 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003361 }
3362 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003363 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003364 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003365 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003366 return true;
3367 }
3368
Mindy Pereirab3112a22012-06-20 12:10:03 -07003369 @VisibleForTesting
3370 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003371 // When switching between reply, reply all, forward,
3372 // follow the behavior of webview.
3373 // The contents of the following fields are cleared
3374 // so that they can be populated directly from the
3375 // ref message:
3376 // 1) Any recipient fields
3377 // 2) The subject
3378 mTo.setText("");
3379 mCc.setText("");
3380 mBcc.setText("");
3381 // Any edits to the subject are replaced with the original subject.
3382 mSubject.setText("");
3383
3384 // Any changes to the contents of the following fields are kept:
3385 // 1) Body
3386 // 2) Attachments
3387 // If the user made changes to attachments, keep their changes.
3388 if (!mAttachmentsChanged) {
3389 mAttachmentsView.deleteAllAttachments();
3390 }
3391 }
3392
Mindy Pereira326c6602012-01-04 15:32:42 -08003393 private class ComposeModeAdapter extends ArrayAdapter<String> {
3394
3395 private LayoutInflater mInflater;
3396
3397 public ComposeModeAdapter(Context context) {
3398 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3399 .getStringArray(R.array.compose_modes));
3400 }
3401
3402 private LayoutInflater getInflater() {
3403 if (mInflater == null) {
3404 mInflater = LayoutInflater.from(getContext());
3405 }
3406 return mInflater;
3407 }
3408
3409 @Override
3410 public View getView(int position, View convertView, ViewGroup parent) {
3411 if (convertView == null) {
3412 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3413 }
3414 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3415 return super.getView(position, convertView, parent);
3416 }
3417 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003418
3419 @Override
3420 public void onRespondInline(String text) {
3421 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003422 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003423 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003424 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003425 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003426 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003427 }
3428
3429 /**
3430 * Append text to the body of the message. If there is no existing body
3431 * text, just sets the body to text.
3432 *
Tony Mantler581edd42014-02-18 15:41:22 -08003433 * @param text Text to append
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003434 * @param withSignature True to append a signature.
3435 */
3436 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003437 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003438 if (bodyText != null && bodyText.length() > 0) {
3439 bodyText.append(text);
3440 } else {
3441 setBody(text, withSignature);
3442 }
3443 }
3444
3445 /**
3446 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003447 *
Tony Mantler581edd42014-02-18 15:41:22 -08003448 * @param text text to set
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003449 * @param withSignature True to append a signature.
3450 */
3451 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003452 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003453 if (withSignature) {
3454 appendSignature();
3455 }
3456 }
3457
3458 private void appendSignature() {
Tony Mantler6a7ac782014-02-19 15:22:02 -08003459 final String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3460 final int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
mindyp27083062012-11-15 09:02:01 -08003461 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003462 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003463 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003464 // Appending a signature does not count as changing text.
3465 mBodyView.removeTextChangedListener(this);
3466 mBodyView.append(convertToPrintableSignature(mSignature));
3467 mBodyView.addTextChangedListener(this);
3468 }
Tony Mantler6a7ac782014-02-19 15:22:02 -08003469 resetBodySelection();
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003470 }
3471 }
3472
3473 private String convertToPrintableSignature(String signature) {
3474 String signatureResource = getResources().getString(R.string.signature);
3475 if (signature == null) {
3476 signature = "";
3477 }
3478 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003479 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003480
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003481 @Override
3482 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003483 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3484 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003485 // Clear a signature, if there was one.
3486 mBodyView.removeTextChangedListener(this);
3487 String oldSignature = mSignature;
3488 String bodyText = getBody().getText().toString();
3489 if (!TextUtils.isEmpty(oldSignature)) {
3490 int pos = getSignatureStartPosition(oldSignature, bodyText);
3491 if (pos > -1) {
3492 mBodyView.setText(bodyText.substring(0, pos));
3493 }
3494 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003495 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003496 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003497 // TODO: handle discarding attachments when switching accounts.
3498 // Only enable save for this draft if there is any other content
3499 // in the message.
3500 if (!isBlank()) {
3501 enableSave(true);
3502 }
3503 mReplyFromChanged = true;
3504 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003505 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003506 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003507
3508 public void enableSave(boolean enabled) {
3509 if (mSave != null) {
3510 mSave.setEnabled(enabled);
3511 }
3512 }
3513
Tony Mantler2558b502013-07-09 10:53:34 -07003514 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003515 // Public no-args constructor needed for fragment re-instantiation
3516 public DiscardConfirmDialogFragment() {}
3517
Tony Mantler2558b502013-07-09 10:53:34 -07003518 @Override
3519 public Dialog onCreateDialog(Bundle savedInstanceState) {
3520 return new AlertDialog.Builder(getActivity())
3521 .setMessage(R.string.confirm_discard_text)
3522 .setPositiveButton(R.string.discard,
3523 new DialogInterface.OnClickListener() {
3524 @Override
3525 public void onClick(DialogInterface dialog, int which) {
3526 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3527 }
3528 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003529 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003530 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003531 }
3532 }
3533
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003534 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003535 final DialogFragment frag = new DiscardConfirmDialogFragment();
3536 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003537 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003538 /**
3539 * Effectively discard the current message.
3540 *
3541 * This method is either invoked from the menu or from the dialog
3542 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003543 */
Tony Mantler2558b502013-07-09 10:53:34 -07003544 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003545 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003546 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3547 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003548 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003549 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003550 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3551 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003552 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003553 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003554 // This is not strictly necessary (since we should not try to
3555 // save the draft after calling this) but it ensures that if we
3556 // do save again for some reason we make a new draft rather than
3557 // trying to resave an expunged draft.
3558 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3559 }
3560 }
3561
Tony Mantler2558b502013-07-09 10:53:34 -07003562 // Display a toast to let the user know
3563 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003564
3565 // This prevents the draft from being saved in onPause().
3566 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003567 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003568 finish();
3569 }
3570
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003571 private void saveIfNeeded() {
3572 if (mAccount == null) {
3573 // We have not chosen an account yet so there's no way that we can save. This is ok,
3574 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3575 // user has not interacted with us yet and there is no real state to save.
3576 return;
3577 }
3578
3579 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003580 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003581 }
3582 }
3583
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003584 @Override
3585 public void onAttachmentDeleted() {
3586 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003587 // If we are showing any attachments, make sure we have an upper
3588 // divider.
3589 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003590 updateSaveUi();
3591 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003592
mindyp40882432012-09-06 11:07:40 -07003593 @Override
3594 public void onAttachmentAdded() {
3595 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3596 mAttachmentsView.focusLastAttachment();
3597 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003598
3599 /**
3600 * This is called any time one of our text fields changes.
3601 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003602 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003603 public void afterTextChanged(Editable s) {
3604 mTextChanged = true;
3605 updateSaveUi();
3606 }
3607
3608 @Override
3609 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3610 // Do nothing.
3611 }
3612
Marc Blank0bbc8582012-04-23 15:07:57 -07003613 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003614 public void onTextChanged(CharSequence s, int start, int before, int count) {
3615 // Do nothing.
3616 }
3617
3618
3619 // There is a big difference between the text associated with an address changing
3620 // to add the display name or to format properly and a recipient being added or deleted.
3621 // Make sure we only notify of changes when a recipient has been added or deleted.
3622 private class RecipientTextWatcher implements TextWatcher {
3623 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3624
3625 private RecipientEditTextView mView;
3626
3627 private TextWatcher mListener;
3628
3629 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3630 mView = view;
3631 mListener = listener;
3632 }
3633
3634 @Override
3635 public void afterTextChanged(Editable s) {
3636 if (hasChanged()) {
3637 mListener.afterTextChanged(s);
3638 }
3639 }
3640
3641 private boolean hasChanged() {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003642 final ArrayList<String> currRecips = buildEmailAddressList(getAddressesFromList(mView));
3643 int totalCount = currRecips.size();
Mindy Pereira75f66632012-01-11 11:42:02 -08003644 int totalPrevCount = 0;
3645 for (Entry<String, Integer> entry : mContent.entrySet()) {
3646 totalPrevCount += entry.getValue();
3647 }
3648 if (totalCount != totalPrevCount) {
3649 return true;
3650 }
3651
3652 for (String recip : currRecips) {
3653 if (!mContent.containsKey(recip)) {
3654 return true;
3655 } else {
3656 int count = mContent.get(recip) - 1;
3657 if (count < 0) {
3658 return true;
3659 } else {
3660 mContent.put(recip, count);
3661 }
3662 }
3663 }
3664 return false;
3665 }
3666
Mindy Pereira75f66632012-01-11 11:42:02 -08003667 @Override
3668 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003669 final ArrayList<String> recips = buildEmailAddressList(getAddressesFromList(mView));
Mindy Pereira75f66632012-01-11 11:42:02 -08003670 for (String recip : recips) {
3671 if (!mContent.containsKey(recip)) {
3672 mContent.put(recip, 1);
3673 } else {
3674 mContent.put(recip, (mContent.get(recip)) + 1);
3675 }
3676 }
3677 }
3678
3679 @Override
3680 public void onTextChanged(CharSequence s, int start, int before, int count) {
3681 // Do nothing.
3682 }
3683 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003684
Andrew Sapperstein1b7ad922014-05-28 11:23:33 -07003685 /**
3686 * Returns a list of email addresses from the recipients. List only contains
3687 * email addresses strips additional info like the recipient's name.
3688 */
3689 private static ArrayList<String> buildEmailAddressList(String[] recips) {
3690 // Tokenize them all and put them in the list.
3691 final ArrayList<String> recipAddresses = Lists.newArrayListWithCapacity(recips.length);
3692 for (int i = 0; i < recips.length; i++) {
3693 recipAddresses.add(Rfc822Tokenizer.tokenize(recips[i])[0].getAddress());
3694 }
3695 return recipAddresses;
3696 }
3697
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003698 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3699 if (sTestSendOrSaveCallback != null && testCallback != null) {
3700 throw new IllegalStateException("Attempting to register more than one test callback");
3701 }
3702 sTestSendOrSaveCallback = testCallback;
3703 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003704
3705 @VisibleForTesting
3706 protected ArrayList<Attachment> getAttachments() {
3707 return mAttachmentsView.getAttachments();
3708 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003709
3710 @Override
3711 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3712 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003713 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3714 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3715 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003716 case REFERENCE_MESSAGE_LOADER:
3717 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3718 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003719 case LOADER_ACCOUNT_CURSOR:
3720 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3721 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003722 }
3723 return null;
3724 }
3725
3726 @Override
3727 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003728 int id = loader.getId();
3729 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003730 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003731 if (data != null && data.moveToFirst()) {
3732 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003733 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003734 initFromRefMessage(mComposeMode);
3735 finishSetup(mComposeMode, intent, null);
3736 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003737 String to = intent.getStringExtra(EXTRA_TO);
3738 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003739 mRefMessage.setTo(null);
3740 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003741 clearChangeListeners();
3742 mTo.append(to);
3743 initChangeListeners();
3744 }
3745 }
3746 } else {
3747 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003748 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003749 break;
Alice Yanga990a712013-03-13 18:37:00 -07003750 case REFERENCE_MESSAGE_LOADER:
3751 // Only populate mRefMessage and leave other fields untouched.
3752 if (data != null && data.moveToFirst()) {
3753 mRefMessage = new Message(data);
3754 }
Andy Huang9f855d62013-05-30 17:15:03 -07003755 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003756 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003757 case LOADER_ACCOUNT_CURSOR:
3758 if (data != null && data.moveToFirst()) {
3759 // there are accounts now!
3760 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003761 final ArrayList<Account> accounts = new ArrayList<Account>();
3762 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003763 do {
3764 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003765 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003766 initializedAccounts.add(account);
3767 }
3768 accounts.add(account);
3769 } while (data.moveToNext());
3770 if (initializedAccounts.size() > 0) {
3771 findViewById(R.id.wait).setVisibility(View.GONE);
3772 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3773 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003774 mAccounts = initializedAccounts.toArray(
3775 new Account[initializedAccounts.size()]);
3776
Mindy Pereirab199d172012-08-13 11:04:03 -07003777 finishCreate();
3778 invalidateOptionsMenu();
3779 } else {
3780 // Show "waiting"
3781 account = accounts.size() > 0 ? accounts.get(0) : null;
3782 showWaitFragment(account);
3783 }
3784 }
3785 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003786 }
3787 }
3788
Mindy Pereirab199d172012-08-13 11:04:03 -07003789 private void showWaitFragment(Account account) {
3790 WaitFragment fragment = getWaitFragment();
3791 if (fragment != null) {
3792 fragment.updateAccount(account);
3793 } else {
3794 findViewById(R.id.wait).setVisibility(View.VISIBLE);
Andy Huangc96efcc2014-04-09 15:30:42 -07003795 replaceFragment(WaitFragment.newInstance(account, false /* expectingMessages */),
Mindy Pereirab199d172012-08-13 11:04:03 -07003796 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3797 }
3798 }
3799
3800 private WaitFragment getWaitFragment() {
3801 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3802 }
3803
3804 private int replaceFragment(Fragment fragment, int transition, String tag) {
3805 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003806 fragmentTransaction.setTransition(transition);
3807 fragmentTransaction.replace(R.id.wait, fragment, tag);
3808 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3809 return transactionId;
3810 }
3811
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003812 @Override
3813 public void onLoaderReset(Loader<Cursor> arg0) {
3814 // Do nothing.
3815 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003816}