blob: 594a73ce4385b128224ad05c2e06d16a7a5ec7bb [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
Mindy Pereira326c6602012-01-04 15:32:42 -080019import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070020import android.app.ActionBar.OnNavigationListener;
21import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080022import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.Dialog;
Tony Mantler2558b502013-07-09 10:53:34 -070025import android.app.DialogFragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070026import android.app.Fragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070027import android.app.FragmentTransaction;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070028import android.app.LoaderManager;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070029import android.content.ClipData;
Mindy Pereira6349a042012-01-04 11:25:01 -080030import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080031import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070033import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080034import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080035import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070036import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080037import android.content.pm.ActivityInfo;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -070038import android.content.res.Resources;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080039import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080040import android.net.Uri;
Andrew Sapperstein05089f32013-10-01 17:00:03 -070041import android.os.Build;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080042import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080043import android.os.Handler;
44import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070045import android.os.ParcelFileDescriptor;
Alice Yang1ebc2db2013-03-14 21:21:44 -070046import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080047import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080048import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.text.Html;
mindyped9c2f02012-10-12 10:02:08 -070050import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080051import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080052import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080053import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080054import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080055import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070056import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070057import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080058import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080059import android.view.Menu;
60import android.view.MenuInflater;
61import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080062import android.view.View;
63import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070064import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080065import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070066import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080067import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080068import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070069import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080070import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080071import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080072
Mindy Pereirac17d0732011-12-29 10:46:19 -080073import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070074import com.android.common.contacts.DataUsageStatUpdater;
Tony Mantler821e5782014-01-06 15:33:43 -080075import com.android.emailcommon.mail.Address;
Andy Huang5c5fd572012-04-08 18:19:29 -070076import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080077import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070078import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070079import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070080import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070081import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080082import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080083import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080084import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080085import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080086import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080087import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070088import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080089import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080090import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070091import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080092import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080093import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070094import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070095import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -070096import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Paul Westbrook83e6b572013-02-05 16:22:42 -080097import com.android.mail.ui.FeedbackEnabledActivity;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -070098import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -070099import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -0700100import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -0700101import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800102import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -0700103import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800104import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800105import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800106import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800107import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800108import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800109
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700110import java.io.FileNotFoundException;
111import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700112import java.io.UnsupportedEncodingException;
113import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800114import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700115import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800116import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800117import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800118import java.util.HashSet;
119import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700120import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700121import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800122import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800123
124public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700125 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700126 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
127 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
128 FeedbackEnabledActivity {
Mindy Pereira6349a042012-01-04 11:25:01 -0800129 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700130 public static final int COMPOSE = -1;
131 public static final int REPLY = 0;
132 public static final int REPLY_ALL = 1;
133 public static final int FORWARD = 2;
134 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800135
136 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700137 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800138
Mindy Pereira326689d2012-05-17 10:14:14 -0700139 private static final String EXTRA_SHOW_CC = "showCc";
140 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800141 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800142 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700143
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700144 private static final String UTF8_ENCODING_NAME = "UTF-8";
145
146 private static final String MAIL_TO = "mailto";
147
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700148 private static final String EXTRA_SUBJECT = "subject";
149
150 private static final String EXTRA_BODY = "body";
151
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700152 /**
153 * Expected to be html formatted text.
154 */
155 private static final String EXTRA_QUOTED_TEXT = "quotedText";
156
mindypd27b6ea2012-10-05 09:43:49 -0700157 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700158
Mark Wei62066e42012-09-13 12:07:02 -0700159 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
160
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700161 // Extra that we can get passed from other activities
Tony Mantler184ec732013-10-24 13:13:49 -0700162 @VisibleForTesting
163 protected static final String EXTRA_TO = "to";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700164 private static final String EXTRA_CC = "cc";
165 private static final String EXTRA_BCC = "bcc";
166
Scott Kennedy60847252013-08-15 15:55:42 -0700167 /**
168 * An optional extra containing a {@link ContentValues} of values to be added to
169 * {@link SendOrSaveMessage#mValues}.
170 */
171 public static final String EXTRA_VALUES = "extra-values";
172
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700173 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700174 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
175 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700176
Mindy Pereira82cc5662012-01-09 17:29:30 -0800177 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
178 // Map containing information about requests to create new messages, and the id of the
179 // messages that were the result of those requests.
180 //
181 // This map is used when the activity that initiated the save a of a new message, is killed
182 // before the save has completed (and when we know the id of the newly created message). When
183 // a save is completed, the service that is running in the background, will update the map
184 //
185 // When a new ComposeActivity instance is created, it will attempt to use the information in
186 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
187 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
188 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700189 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800190 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800191 /**
192 * Notifies the {@code Activity} that the caller is an Email
193 * {@code Activity}, so that the back behavior may be modified accordingly.
194 *
195 * @see #onAppUpPressed
196 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700197 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800198
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700199 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700200
Scott Kennedy5680ec22013-01-07 13:15:20 -0800201 /** If set, we will clear notifications for this folder. */
202 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
203
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800204 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700205 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700206 // If this is a reply/forward then this extra will hold a uri we must query
207 // to get the original message.
208 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700209 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700210 // draft message
211 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800212 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700213 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800214 // Request numbers for activities we start
215 private static final int RESULT_PICK_ATTACHMENT = 1;
216 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700217 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700218 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700219
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700220 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
221 private static final String EXTRA_REQUEST_ID = "requestId";
222 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700223 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700224 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700225 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700226 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700227 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700228 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700229 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700230 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700231 private static final String MIME_TYPE_PHOTO = "image/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800232
Andy Huang9f855d62013-05-30 17:15:03 -0700233 private static final String KEY_INNER_SAVED_STATE = "compose_state";
234
Mindy Pereira82cc5662012-01-09 17:29:30 -0800235 /**
236 * A single thread for running tasks in the background.
237 */
238 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800239 private RecipientEditTextView mTo;
240 private RecipientEditTextView mCc;
241 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800242 private Button mCcBccButton;
243 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800244 private AttachmentsView mAttachmentsView;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700245 protected Account mAccount;
Tony Mantler59e69092013-08-14 11:05:00 -0700246 protected ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800247 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800248 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800249 private TextView mSubject;
250
Mindy Pereira326c6602012-01-04 15:32:42 -0800251 private ComposeModeAdapter mComposeModeAdapter;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700252 protected int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800253 private boolean mForward;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800254 private QuotedTextView mQuotedTextView;
Tony Mantler59e69092013-08-14 11:05:00 -0700255 protected EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800256 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800257 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800258 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700259 @VisibleForTesting
260 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800261 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800262 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800263 private boolean mTextChanged;
264 private boolean mReplyFromChanged;
265 private MenuItem mSave;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700266 @VisibleForTesting
267 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800268 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
269 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800270 private ReplyFromAccount mDraftAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800271 private Object mDraftLock = new Object();
Andrew Sapperstein6aea7862013-10-24 19:59:51 -0700272 private View mAddAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800273
Mindy Pereira326c6602012-01-04 15:32:42 -0800274 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700275 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
276 */
277 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700278 private RecipientTextWatcher mToListener;
279 private RecipientTextWatcher mCcListener;
280 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700281 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700282 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700283 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700284 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700285
mindyp1623f9b2012-11-21 12:41:16 -0800286 // Array of the outstanding send or save tasks. Access is synchronized
287 // with the object itself
288 /* package for testing */
289 @VisibleForTesting
290 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
291 // FIXME: this variable is never read. related to sRequestMessageIdMap.
292 private int mRequestId;
293 private String mSignature;
294 private Account[] mAccounts;
295 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700296 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800297
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700298 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800299 * Can be called from a non-UI thread.
300 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800301 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700302 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
303 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800304 }
305
Mindy Pereira6349a042012-01-04 11:25:01 -0800306 /**
307 * Can be called from a non-UI thread.
308 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800309 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700310 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800311 }
312
313 /**
314 * Can be called from a non-UI thread.
315 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700316 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700317 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
318 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700319 }
320
321 /**
322 * Can be called from a non-UI thread.
323 */
324 public static void composeWithQuotedText(Context launcher, Account account,
Scott Kennedy60847252013-08-15 15:55:42 -0700325 String quotedText, String subject, final ContentValues extraValues) {
326 launch(launcher, account, null, COMPOSE, null, null, quotedText, subject, extraValues);
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700327 }
328
329 /**
330 * Can be called from a non-UI thread.
331 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700332 public static void composeWithExtraValues(Context launcher, Account account,
333 String subject, final ContentValues extraValues) {
334 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
335 }
336
337 /**
338 * Can be called from a non-UI thread.
339 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800340 public static Intent createReplyIntent(final Context launcher, final Account account,
341 final Uri messageUri, final boolean isReplyAll) {
342 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
343 }
344
345 /**
346 * Can be called from a non-UI thread.
347 */
348 public static Intent createForwardIntent(final Context launcher, final Account account,
349 final Uri messageUri) {
350 return createActionIntent(launcher, account, messageUri, FORWARD);
351 }
352
353 private static Intent createActionIntent(final Context launcher, final Account account,
354 final Uri messageUri, final int action) {
355 final Intent intent = new Intent(launcher, ComposeActivity.class);
356
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700357 updateActionIntent(account, messageUri, action, intent);
358
359 return intent;
360 }
361
362 @VisibleForTesting
363 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800364 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
365 intent.putExtra(EXTRA_ACTION, action);
366 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
367 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
368
369 return intent;
370 }
371
372 /**
373 * Can be called from a non-UI thread.
374 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800375 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700376 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800377 }
378
379 /**
380 * Can be called from a non-UI thread.
381 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800382 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700383 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
384 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800385 }
386
387 /**
388 * Can be called from a non-UI thread.
389 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800390 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700391 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800392 }
393
Alice Yang1ebc2db2013-03-14 21:21:44 -0700394 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
395 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700396 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700397 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700398 }
399
400 private static void launch(Context launcher, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700401 String toAddress, String body, String quotedText, String subject,
402 final ContentValues extraValues) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800403 Intent intent = new Intent(launcher, ComposeActivity.class);
404 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
405 intent.putExtra(EXTRA_ACTION, action);
406 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700407 if (action == EDIT_DRAFT) {
408 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
409 } else {
410 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
411 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700412 if (toAddress != null) {
413 intent.putExtra(EXTRA_TO, toAddress);
414 }
415 if (body != null) {
416 intent.putExtra(EXTRA_BODY, body);
417 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700418 if (quotedText != null) {
419 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
420 }
421 if (subject != null) {
422 intent.putExtra(EXTRA_SUBJECT, subject);
423 }
Scott Kennedy60847252013-08-15 15:55:42 -0700424 if (extraValues != null) {
425 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
426 intent.putExtra(EXTRA_VALUES, extraValues);
427 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800428 launcher.startActivity(intent);
429 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800430
Andy Huang0a2a3462013-12-20 15:56:13 -0800431 public static void composeMailto(Context launcher, Account account, Uri mailto) {
432 final Intent intent = new Intent(Intent.ACTION_VIEW, mailto, launcher,
433 ComposeActivity.class);
434 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
435 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
436 launcher.startActivity(intent);
437 }
438
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800439 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700440 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800441 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800442 setContentView(R.layout.compose);
Andy Huang9f855d62013-05-30 17:15:03 -0700443 mInnerSavedState = (savedInstanceState != null) ?
444 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700445 checkValidAccounts();
446 }
447
448 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700449 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800450 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800451 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700452 Message message;
Mark Wei62066e42012-09-13 12:07:02 -0700453 ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700454 mShowQuotedText = false;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700455 CharSequence quotedText = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700456 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700457 // Check for any of the possibly supplied accounts.;
458 Account account = null;
Andy Huang9f855d62013-05-30 17:15:03 -0700459 if (hadSavedInstanceStateMessage(savedState)) {
460 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
461 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
462 message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700463
Andy Huang9f855d62013-05-30 17:15:03 -0700464 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
465 mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700466 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700467
468 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700469 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700470 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700471 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
472 // Initialize the message from the message in the intent
473 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700474 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700475 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700476 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
Andy Huang4fe0af82013-08-20 17:24:51 -0700477
478 if (Analytics.isLoggable()) {
479 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
480 Analytics.getInstance().sendEvent(
481 "notification_action", "compose", getActionString(action), 0);
482 }
483 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700484 }
Mark Wei62066e42012-09-13 12:07:02 -0700485 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700486
487 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800488 if (mAccount == null) {
489 return;
490 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700491
Scott Kennedyfe853d32013-06-19 11:47:35 -0700492 initRecipients();
493
Scott Kennedy5680ec22013-01-07 13:15:20 -0800494 // Clear the notification and mark the conversation as seen, if necessary
495 final Folder notificationFolder =
496 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
497 if (notificationFolder != null) {
498 final Intent clearNotifIntent =
499 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800500 clearNotifIntent.setPackage(getPackageName());
Scott Kennedy48cfe462013-04-10 11:32:02 -0700501 clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
502 clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800503
504 startService(clearNotifIntent);
505 }
506
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700507 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
508 mLaunchedFromEmail = true;
509 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
510 final Uri dataUri = intent.getData();
511 if (dataUri != null) {
512 final String dataScheme = intent.getData().getScheme();
513 final String accountScheme = mAccount.composeIntentUri.getScheme();
514 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
515 }
516 }
517
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700518 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700519 mShowQuotedText = true;
520 mComposeMode = action;
521 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700522 return;
523 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700524 initFromDraftMessage(message);
525 initQuotedTextFromRefMessage(mRefMessage, action);
Andy Huang9f855d62013-05-30 17:15:03 -0700526 showCcBcc(savedState);
Alice Yanga990a712013-03-13 18:37:00 -0700527 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700528 // if we should be showing quoted text but mRefMessage is null
529 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700530 if (mShowQuotedText && mRefMessage == null) {
531 if (quotedText != null) {
532 initQuotedText(quotedText, false /* shouldQuoteText */);
533 } else if (mExtraValues != null) {
534 initExtraValues(mExtraValues);
535 return;
536 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700537 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700538 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700539 initFromDraftMessage(message);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800540 boolean showBcc = !TextUtils.isEmpty(message.getBcc());
541 boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
Mindy Pereiraef388302012-06-18 19:07:44 -0700542 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700543 // Update the action to the draft type of the previous draft
544 switch (message.draftType) {
545 case UIProvider.DraftType.REPLY:
546 action = REPLY;
547 break;
548 case UIProvider.DraftType.REPLY_ALL:
549 action = REPLY_ALL;
550 break;
551 case UIProvider.DraftType.FORWARD:
552 action = FORWARD;
553 break;
554 case UIProvider.DraftType.COMPOSE:
555 default:
556 action = COMPOSE;
557 break;
558 }
Alice Yanga990a712013-03-13 18:37:00 -0700559 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
560
561 mShowQuotedText = message.appendRefMessageContent;
562 if (message.refMessageUri != null) {
563 // If we're editing an existing draft that was in reference to an existing message,
564 // still need to load that original message since we might need to refer to the
565 // original sender and recipients if user switches "reply <-> reply-all".
566 mRefMessageUri = message.refMessageUri;
567 mComposeMode = action;
568 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
569 return;
570 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700571 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
572 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800573 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700574 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700575 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700576 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700577 if (initFromExtras(intent)) {
578 return;
579 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700580 }
Alice Yanga990a712013-03-13 18:37:00 -0700581
582 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700583 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700584 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700585
Mindy Pereirab199d172012-08-13 11:04:03 -0700586 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700587 final Account[] allAccounts = AccountUtils.getAccounts(this);
588 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700589 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
590 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700591 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700592 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
593 }
594 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700595 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700596 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700597 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700598 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700599 anySyncing = true;
600 break;
601 }
602 }
603 if (!anySyncing) {
604 // There are accounts, but none are sync'd, which is just like having no accounts.
605 mAccounts = null;
606 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
607 return;
608 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700609 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700610 finishCreate();
611 }
612 }
613
Mindy Pereira47d0e652012-07-23 09:45:07 -0700614 private Account obtainAccount(Intent intent) {
615 Account account = null;
616 Object accountExtra = null;
617 if (intent != null && intent.getExtras() != null) {
618 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
619 if (accountExtra instanceof Account) {
620 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700621 } else if (accountExtra instanceof String) {
622 // This is the Account attached to the widget compose intent.
623 account = Account.newinstance((String)accountExtra);
624 if (account != null) {
625 return account;
626 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700627 }
mindyp5ee9dc42013-01-08 09:54:54 -0800628 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
629 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
630 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700631 }
632 if (account == null) {
mindyp06174462012-10-12 09:11:27 -0700633 MailAppProvider provider = MailAppProvider.getInstance();
634 String lastAccountUri = provider.getLastSentFromAccount();
635 if (TextUtils.isEmpty(lastAccountUri)) {
636 lastAccountUri = provider.getLastViewedAccount();
637 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700638 if (!TextUtils.isEmpty(lastAccountUri)) {
639 accountExtra = Uri.parse(lastAccountUri);
640 }
641 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700642 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700643 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
644 // For backwards compatibility, we need to check account
645 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700646 for (Account a : mAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -0700647 if (a.getEmailAddress().equals(accountExtra)) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700648 account = a;
649 }
650 }
651 } else if (accountExtra instanceof Uri) {
652 // The uri of the last viewed account is what is stored in
653 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700654 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700655 if (a.uri.equals(accountExtra)) {
656 account = a;
657 }
658 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700659 }
660 if (account == null) {
661 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700662 }
663 }
664 return account;
665 }
666
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700667 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800668 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700669 // Don't bother with the intent if we have procured a message from the
670 // intent already.
671 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
672 initAttachmentsFromIntent(intent);
673 }
Alice Yanga990a712013-03-13 18:37:00 -0700674 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700675 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
676 action);
mindypd4a48662012-11-08 17:13:49 -0800677
678 // If this is a draft message, the draft account is whatever account was
679 // used to open the draft message in Compose.
680 if (mDraft != null) {
681 mDraftAccount = mReplyFromAccount;
682 }
683
Mindy Pereira75f66632012-01-11 11:42:02 -0800684 initChangeListeners();
Mindy Pereira326689d2012-05-17 10:14:14 -0700685 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700686 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800687
Andy Huang9f855d62013-05-30 17:15:03 -0700688 mRespondedInline = mInnerSavedState != null ?
689 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
mindyp1623f9b2012-11-21 12:41:16 -0800690 if (mRespondedInline) {
691 mQuotedTextView.setVisibility(View.GONE);
692 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700693 }
694
Scott Kennedyff8553f2013-04-05 20:57:44 -0700695 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700696 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
697 }
698
Mindy Pereira71c9e562012-05-17 11:01:02 -0700699 private void updateHideOrShowQuotedText(boolean showQuotedText) {
700 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700701 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700702 }
703
704 private void setFocus(int action) {
705 if (action == EDIT_DRAFT) {
706 int type = mDraft.draftType;
707 switch (type) {
708 case UIProvider.DraftType.COMPOSE:
709 case UIProvider.DraftType.FORWARD:
710 action = COMPOSE;
711 break;
712 case UIProvider.DraftType.REPLY:
713 case UIProvider.DraftType.REPLY_ALL:
714 default:
715 action = REPLY;
716 break;
717 }
718 }
719 switch (action) {
720 case FORWARD:
721 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800722 if (TextUtils.isEmpty(mTo.getText())) {
723 mTo.requestFocus();
724 break;
725 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700726 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700727 case REPLY:
728 case REPLY_ALL:
729 default:
730 focusBody();
731 break;
732 }
733 }
734
735 /**
736 * Focus the body of the message.
737 */
738 public void focusBody() {
739 mBodyView.requestFocus();
740 int length = mBodyView.getText().length();
741
742 int signatureStartPos = getSignatureStartPosition(
743 mSignature, mBodyView.getText().toString());
744 if (signatureStartPos > -1) {
745 // In case the user deleted the newlines...
746 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700747 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700748 // Move cursor to the end.
749 mBodyView.setSelection(length);
750 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800751 }
752
753 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700754 protected void onStart() {
755 super.onStart();
756
757 Analytics.getInstance().activityStart(this);
758 }
759
760 @Override
761 protected void onStop() {
762 super.onStop();
763
764 Analytics.getInstance().activityStop(this);
765 }
766
767 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800768 protected void onResume() {
769 super.onResume();
770 // Update the from spinner as other accounts
771 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800772 if (mFromSpinner != null && mAccount != null) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700773 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800774 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800775 }
776
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800777 @Override
778 protected void onPause() {
779 super.onPause();
780
Mindy Pereiraa2148332012-07-02 13:54:14 -0700781 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700782 // Don't save unnecessary drafts if we are only changing the orientation.
783 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700784 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700785
Andy Huange003b4c2013-08-16 10:32:05 -0700786 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700787 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
788 // because that method can be invoked many times in a single compose session.)
789 logSendOrSave(true /* save */);
790 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700791 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800792 }
793
794 @Override
795 protected final void onActivityResult(int request, int result, Intent data) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700796 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800797 addAttachmentAndUpdateView(data);
Mindy Pereirab199d172012-08-13 11:04:03 -0700798 mAddingAttachment = false;
799 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700800 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700801 if (result != RESULT_OK) {
802 finish();
803 } else {
804 // Watch for accounts to show up!
805 // restart the loader to get the updated list of accounts
806 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
807 showWaitFragment(null);
808 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800809 }
810 }
811
812 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700813 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700814 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
815 if (hasAccounts) {
816 clearChangeListeners();
817 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700818 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700819 if (mInnerSavedState != null) {
820 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
821 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
822 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700823 // There should be a focus and it should be an EditText since we
824 // only save these extras if these conditions are true.
825 EditText focusEditText = (EditText) getCurrentFocus();
826 final int length = focusEditText.getText().length();
827 if (selectionStart < length && selectionEnd < length) {
828 focusEditText.setSelection(selectionStart, selectionEnd);
829 }
830 }
831 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700832 if (hasAccounts) {
833 initChangeListeners();
834 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700835 }
836
837 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700838 protected final void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800839 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700840 final Bundle inner = new Bundle();
841 saveState(inner);
842 state.putBundle(KEY_INNER_SAVED_STATE, inner);
843 }
844
845 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700846 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
847 if (mAccounts == null || mAccounts.length == 0) {
848 return;
849 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700850 // The framework is happy to save and restore the selection but only if it also saves and
851 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
852 // this manually.
853 View focus = getCurrentFocus();
854 if (focus != null && focus instanceof EditText) {
855 EditText focusEditText = (EditText) focus;
856 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
857 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
858 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700859
860 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700861 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700862 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
863 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
864 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700865 if (selectedReplyFromAccount != null) {
866 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
867 .toString());
868 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
869 } else {
870 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
871 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800872
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700873 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
874 // We don't have a draft id, and we have a request id,
875 // save the request id.
876 state.putInt(EXTRA_REQUEST_ID, mRequestId);
877 }
878
879 // We want to restore the current mode after a pause
880 // or rotation.
881 int mode = getMode();
882 state.putInt(EXTRA_ACTION, mode);
883
mindype7b76aa2012-11-14 16:19:13 -0800884 final Message message = createMessage(selectedReplyFromAccount, mode);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700885 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -0800886 message.id = mDraft.id;
887 message.serverId = mDraft.serverId;
888 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700889 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700890 state.putParcelable(EXTRA_MESSAGE, message);
891
892 if (mRefMessage != null) {
893 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700894 } else if (message.appendRefMessageContent) {
895 // If we have no ref message but should be appending
896 // ref message content, we have orphaned quoted text. Save it.
897 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700898 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700899 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
900 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -0800901 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -0800902 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -0700903 state.putParcelableArrayList(
904 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -0700905
906 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700907 }
908
909 private int getMode() {
910 int mode = ComposeActivity.COMPOSE;
911 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700912 if (actionBar != null
913 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700914 mode = actionBar.getSelectedNavigationIndex();
915 }
916 return mode;
917 }
918
919 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
920 Message message = new Message();
921 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -0700922 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700923 message.uri = null;
924 message.conversationUri = null;
925 message.subject = mSubject.getText().toString();
926 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800927 message.setTo(formatSenders(mTo.getText().toString()));
928 message.setCc(formatSenders(mCc.getText().toString()));
929 message.setBcc(formatSenders(mBcc.getText().toString()));
930 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700931 message.dateReceivedMs = 0;
Paul Westbrookb4931c62013-01-14 17:51:18 -0800932 final String htmlBody = Html.toHtml(removeComposingSpans(mBodyView.getText()));
933 final StringBuilder fullBody = new StringBuilder(htmlBody);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700934 message.bodyHtml = fullBody.toString();
935 message.bodyText = mBodyView.getText().toString();
936 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -0700937 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700938 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
939 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
940 message.hasAttachments = attachments != null && attachments.size() > 0;
941 message.attachmentListUri = null;
942 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700943 message.alwaysShowImages = false;
944 message.attachmentsJson = Attachment.toJSONArray(attachments);
945 CharSequence quotedText = mQuotedTextView.getQuotedText();
946 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
947 .getQuotedTextOffset(quotedText.toString()) : -1;
948 message.accountUri = null;
Tony Mantlerbb036ff72013-10-18 14:03:43 -0700949 final String email = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
950 : mAccount != null ? mAccount.getEmailAddress() : null;
Tony Mantlerf441d142013-10-22 11:46:00 -0700951 final String senderName = selectedReplyFromAccount != null ? selectedReplyFromAccount.name
952 : mAccount != null ? mAccount.getSenderName() : null;
Tony Mantler821e5782014-01-06 15:33:43 -0800953 final Address address = new Address(email, senderName);
Tony Mantlerf441d142013-10-22 11:46:00 -0700954 message.setFrom(address.toHeader());
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700955 message.draftType = getDraftType(mode);
mindype7b76aa2012-11-14 16:19:13 -0800956 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700957 }
958
Scott Kennedyff8553f2013-04-05 20:57:44 -0700959 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -0700960 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
961 return string.substring(0, string.length() - 1);
962 }
963 return string;
964 }
965
Mindy Pereira818143e2012-01-11 13:59:49 -0800966 @VisibleForTesting
967 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700968 if (account == null) {
969 return;
970 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700971 if (!account.equals(mAccount)) {
972 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700973 mCachedSettings = mAccount.settings;
974 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700975 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700976 if (mAccount != null) {
Tony Mantler79b11562013-10-09 15:31:50 -0700977 MailActivity.setNfcMessage(mAccount.getEmailAddress());
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700978 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800979 }
980
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700981 private void initFromSpinner(Bundle bundle, int action) {
982 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700983 action = COMPOSE;
984 }
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700985 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
986
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700987 if (bundle != null) {
988 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
989 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
990 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
991 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700992 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700993 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
994 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700995 }
996 if (mReplyFromAccount == null) {
997 if (mDraft != null) {
998 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
999 } else if (mRefMessage != null) {
1000 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
1001 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001002 }
1003 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -07001004 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001005 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001006
Mindy Pereira62de1b12012-04-06 12:17:56 -07001007 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -07001008
Mindy Pereira62de1b12012-04-06 12:17:56 -07001009 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001010 // If there is only 1 account, just show that account.
1011 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001012 // send mail from / save drafts to.
1013 mFromStatic.setVisibility(View.GONE);
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001014 // TODO: do we want name or address here?
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001015 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001016 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001017 } else {
1018 mFromStatic.setVisibility(View.VISIBLE);
Tony Mantlerbb036ff72013-10-18 14:03:43 -07001019 // TODO: do we want name or address here?
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001020 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001021 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001022 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001023 }
1024
Mindy Pereira62de1b12012-04-06 12:17:56 -07001025 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1026 if (refMessage.accountUri != null) {
1027 // This must be from combined inbox.
1028 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1029 for (ReplyFromAccount from : replyFromAccounts) {
1030 if (from.account.uri.equals(refMessage.accountUri)) {
1031 return from;
1032 }
1033 }
1034 return null;
1035 } else {
1036 return getReplyFromAccount(account, refMessage);
1037 }
1038 }
1039
1040 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001041 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001042 * return who the message should be sent from.
1043 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001044 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001045 * @return the address from which to reply.
1046 */
1047 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1048 // First see if we are supposed to use the default address or
1049 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001050 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001051 return getDefaultReplyFromAccount(account);
1052 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001053 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001054 // all the message recipients and find one that matches
1055 // a custom from or account.
1056 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001057 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1058 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001059 return getMatchingRecipient(account, allRecipients);
1060 }
1061 }
1062
1063 /**
1064 * Compare all the recipients of an email to the current account and all
1065 * custom addresses associated with that account. Return the match if there
1066 * is one, or the default account if there isn't.
1067 */
1068 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1069 // Tokenize the list and place in a hashmap.
1070 ReplyFromAccount matchingReplyFrom = null;
1071 Rfc822Token[] tokens;
1072 HashSet<String> recipientsMap = new HashSet<String>();
1073 for (String address : sentTo) {
1074 tokens = Rfc822Tokenizer.tokenize(address);
1075 for (int i = 0; i < tokens.length; i++) {
1076 recipientsMap.add(tokens[i].getAddress());
1077 }
1078 }
1079
1080 int matchingAddressCount = 0;
1081 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001082 customFroms = account.getReplyFroms();
1083 if (customFroms != null) {
1084 for (ReplyFromAccount entry : customFroms) {
1085 if (recipientsMap.contains(entry.address)) {
1086 matchingReplyFrom = entry;
1087 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001088 }
1089 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001090 }
1091 if (matchingAddressCount > 1) {
1092 matchingReplyFrom = getDefaultReplyFromAccount(account);
1093 }
1094 return matchingReplyFrom;
1095 }
1096
Scott Kennedyff8553f2013-04-05 20:57:44 -07001097 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1098 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001099 if (from.isDefault) {
1100 return from;
1101 }
1102 }
Tony Mantlerf441d142013-10-22 11:46:00 -07001103 return new ReplyFromAccount(account, account.uri, account.getEmailAddress(),
1104 account.getSenderName(), account.getEmailAddress(), true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001105 }
1106
Tony Mantlerf441d142013-10-22 11:46:00 -07001107 private ReplyFromAccount getReplyFromAccountFromDraft(final Account account,
1108 final Message msg) {
1109 final Address[] draftFroms = Address.parse(msg.getFrom());
1110 final String sender = draftFroms.length > 0 ? draftFroms[0].getAddress() : "";
Mindy Pereira62de1b12012-04-06 12:17:56 -07001111 ReplyFromAccount replyFromAccount = null;
1112 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Tony Mantler79b11562013-10-09 15:31:50 -07001113 if (TextUtils.equals(account.getEmailAddress(), sender)) {
Tony Mantlerf441d142013-10-22 11:46:00 -07001114 replyFromAccount = getDefaultReplyFromAccount(account);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001115 } else {
1116 for (ReplyFromAccount fromAccount : replyFromAccounts) {
Tony Mantler79b11562013-10-09 15:31:50 -07001117 if (TextUtils.equals(fromAccount.address, sender)) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001118 replyFromAccount = fromAccount;
1119 break;
1120 }
1121 }
1122 }
1123 return replyFromAccount;
1124 }
1125
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001126 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001127 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001128 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001129 if (mCcBccButton != null) {
1130 mCcBccButton.setOnClickListener(this);
1131 }
1132 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001133 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Andrew Sapperstein6aea7862013-10-24 19:59:51 -07001134 mAddAttachmentsButton = findViewById(R.id.add_attachment);
1135 if (mAddAttachmentsButton != null) {
1136 mAddAttachmentsButton.setOnClickListener(this);
mindypcd0b0b92012-08-23 14:33:17 -07001137 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001138 mTo = (RecipientEditTextView) findViewById(R.id.to);
Scott Kennedy41500392013-04-24 18:46:36 -07001139 mTo.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001140 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Scott Kennedy41500392013-04-24 18:46:36 -07001141 mCc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001142 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Scott Kennedy41500392013-04-24 18:46:36 -07001143 mBcc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001144 // TODO: add special chips text change watchers before adding
1145 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001146 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001147 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001148 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1149 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001150 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001151 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001152 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001153 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001154 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001155 }
1156
mindyp62d3ec72012-08-24 13:04:09 -07001157 @Override
1158 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1159 if (action == EditorInfo.IME_ACTION_DONE) {
1160 focusBody();
1161 return true;
1162 }
1163 return false;
1164 }
1165
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001166 protected TextView getBody() {
1167 return mBodyView;
1168 }
1169
1170 @VisibleForTesting
Andy Huang0a2a3462013-12-20 15:56:13 -08001171 public String getBodyHtml() {
1172 return Html.toHtml(removeComposingSpans(mBodyView.getText()));
1173 }
1174
1175 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001176 public Account getFromAccount() {
1177 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1178 mReplyFromAccount.account : mAccount;
1179 }
1180
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001181 private void clearChangeListeners() {
1182 mSubject.removeTextChangedListener(this);
1183 mBodyView.removeTextChangedListener(this);
1184 mTo.removeTextChangedListener(mToListener);
1185 mCc.removeTextChangedListener(mCcListener);
1186 mBcc.removeTextChangedListener(mBccListener);
1187 mFromSpinner.setOnAccountChangedListener(null);
1188 mAttachmentsView.setAttachmentChangesListener(null);
1189 }
1190
Mindy Pereira75f66632012-01-11 11:42:02 -08001191 // Now that the message has been initialized from any existing draft or
1192 // ref message data, set up listeners for any changes that occur to the
1193 // message.
1194 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001195 // Make sure we only add text changed listeners once!
1196 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001197 mSubject.addTextChangedListener(this);
1198 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001199 if (mToListener == null) {
1200 mToListener = new RecipientTextWatcher(mTo, this);
1201 }
1202 mTo.addTextChangedListener(mToListener);
1203 if (mCcListener == null) {
1204 mCcListener = new RecipientTextWatcher(mCc, this);
1205 }
1206 mCc.addTextChangedListener(mCcListener);
1207 if (mBccListener == null) {
1208 mBccListener = new RecipientTextWatcher(mBcc, this);
1209 }
1210 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001211 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001212 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001213 }
1214
Alice Yanga990a712013-03-13 18:37:00 -07001215 private void initActionBar() {
1216 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001217 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001218 if (actionBar == null) {
1219 return;
1220 }
Alice Yanga990a712013-03-13 18:37:00 -07001221 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001222 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1223 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001224 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001225 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001226 if (mComposeModeAdapter == null) {
1227 mComposeModeAdapter = new ComposeModeAdapter(this);
1228 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001229 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1230 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001231 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001232 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001233 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001234 break;
1235 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001236 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001237 break;
1238 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001239 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001240 break;
1241 }
1242 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001243 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1244 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1245 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001246 }
1247
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001248 private void initFromRefMessage(int action) {
1249 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001250
1251 // Check if To: address and email body needs to be prefilled based on extras.
1252 // This is used for reporting rendering feedback.
1253 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1254 Intent intent = getIntent();
1255 if (intent.getExtras() != null) {
1256 String toAddresses = intent.getStringExtra(EXTRA_TO);
1257 if (toAddresses != null) {
1258 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1259 }
1260 String body = intent.getStringExtra(EXTRA_BODY);
1261 if (body != null) {
1262 setBody(body, false /* withSignature */);
1263 }
1264 }
1265 }
1266
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001267 if (mRefMessage != null) {
1268 // CC field only gets populated when doing REPLY_ALL.
1269 // BCC never gets auto-populated, unless the user is editing
1270 // a draft with one.
Mindy Pereira29a717e2012-07-25 18:05:48 -07001271 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001272 mCcBccView.show(false, true, false);
1273 }
1274 }
1275 updateHideOrShowCcBcc();
1276 }
1277
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001278 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001279 setSubject(mRefMessage, action);
1280 // Setup recipients
1281 if (action == FORWARD) {
1282 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001283 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001284 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001285 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001286 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1287 initAttachments(mRefMessage);
1288 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001289 }
1290
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001291 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001292 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001293
1294 mDraft = message;
1295 mDraftId = message.id;
1296 mSubject.setText(message.subject);
1297 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001298 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001299 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001300 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1301 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001302 if (message.hasAttachments) {
1303 List<Attachment> attachments = message.getAttachments();
1304 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001305 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001306 }
1307 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -07001308 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -07001309 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001310 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001311 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001312 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001313 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001314 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001315 // Find the offset in the htmltext of the actual quoted text and strip it out.
1316 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1317 if (quotedTextIndex > -1) {
mindypc59dd822012-11-13 10:56:21 -08001318 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1319 quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001320 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1321 message.bodyHtml.length());
1322 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001323 } else {
mindypc59dd822012-11-13 10:56:21 -08001324 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001325 }
1326 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001327 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001328 final String body = message.bodyText;
1329 final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1330 (quotedTextIndex > -1 ?
1331 message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1332 : "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001333 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001334 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1335 : null;
Mindy Pereira002ff522012-05-30 10:31:26 -07001336 }
1337 mBodyView.setText(bodyText);
1338 }
1339 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001340 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001341 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001342 }
1343
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001344 /**
1345 * Fill all the widgets with the content found in the Intent Extra, if any.
1346 * Also apply the same style to all widgets. Note: if initFromExtras is
1347 * called as a result of switching between reply, reply all, and forward per
1348 * the latest revision of Gmail, and the user has already made changes to
1349 * attachments on a previous incarnation of the message (as a reply, reply
1350 * all, or forward), the original attachments from the message will not be
1351 * re-instantiated. The user's changes will be respected. This follows the
1352 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001353 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001354 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001355 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001356 // If we were invoked with a SENDTO intent, the value
1357 // should take precedence
1358 final Uri dataUri = intent.getData();
1359 if (dataUri != null) {
1360 if (MAIL_TO.equals(dataUri.getScheme())) {
1361 initFromMailTo(dataUri.toString());
1362 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001363 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001364 String toText = dataUri.getSchemeSpecificPart();
1365 if (toText != null) {
1366 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001367 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001368 }
1369 }
1370 }
1371 }
1372
1373 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1374 if (extraStrings != null) {
1375 addToAddresses(Arrays.asList(extraStrings));
1376 }
1377 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1378 if (extraStrings != null) {
1379 addCcAddresses(Arrays.asList(extraStrings), null);
1380 }
1381 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1382 if (extraStrings != null) {
1383 addBccAddresses(Arrays.asList(extraStrings));
1384 }
1385
1386 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1387 if (extraString != null) {
1388 mSubject.setText(extraString);
1389 }
1390
1391 for (String extra : ALL_EXTRAS) {
1392 if (intent.hasExtra(extra)) {
1393 String value = intent.getStringExtra(extra);
1394 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001395 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001396 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001397 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001398 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001399 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001400 } else if (EXTRA_SUBJECT.equals(extra)) {
1401 mSubject.setText(value);
1402 } else if (EXTRA_BODY.equals(extra)) {
1403 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001404 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1405 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001406 }
1407 }
1408 }
1409
1410 Bundle extras = intent.getExtras();
1411 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001412 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1413 if (text != null) {
1414 setBody(text, true /* with signature */);
1415 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001416
1417 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001418 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001419
1420 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1421 if (mExtraValues != null) {
1422 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1423 initExtraValues(mExtraValues);
1424 return true;
1425 }
1426
1427 return false;
1428 }
1429
1430 protected void initExtraValues(ContentValues extraValues) {
1431 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001432 }
1433
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001434
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001435 @VisibleForTesting
1436 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001437 // TODO: handle the case where there are spaces in the display name as
1438 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1439 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001440 // Since URLDecode.decode changes + into ' ', and + is a valid
1441 // email character, we need to find/ replace these ourselves before
1442 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001443 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001444 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001445 } catch (IllegalArgumentException e) {
1446 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1447 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1448 } else {
1449 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1450 }
1451 return null;
1452 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001453 }
1454
1455 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001456 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1457 * changing '+' into ' '
1458 *
1459 * @param toReplace Input string
1460 * @return The string with all "+" characters replaced with "%2B"
1461 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001462 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001463 return toReplace.replace("+", "%2B");
1464 }
1465
1466 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001467 * Initialize the compose view from a String representing a mailTo uri.
1468 * @param mailToString The uri as a string.
1469 */
1470 public void initFromMailTo(String mailToString) {
1471 // We need to disguise this string as a URI in order to parse it
1472 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1473 Uri uri = Uri.parse("foo://" + mailToString);
1474 int index = mailToString.indexOf("?");
1475 int length = "mailto".length() + 1;
1476 String to;
1477 try {
1478 // Extract the recipient after mailto:
1479 if (index == -1) {
1480 to = decodeEmailInUri(mailToString.substring(length));
1481 } else {
1482 to = decodeEmailInUri(mailToString.substring(length, index));
1483 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001484 if (!TextUtils.isEmpty(to)) {
1485 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1486 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001487 } catch (UnsupportedEncodingException e) {
1488 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1489 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1490 } else {
1491 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1492 }
1493 }
1494
1495 List<String> cc = uri.getQueryParameters("cc");
1496 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1497
1498 List<String> otherTo = uri.getQueryParameters("to");
1499 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1500
1501 List<String> bcc = uri.getQueryParameters("bcc");
1502 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1503
1504 List<String> subject = uri.getQueryParameters("subject");
1505 if (subject.size() > 0) {
1506 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001507 mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
1508 UTF8_ENCODING_NAME));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001509 } catch (UnsupportedEncodingException e) {
1510 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1511 e.getMessage(), subject);
1512 }
1513 }
1514
1515 List<String> body = uri.getQueryParameters("body");
1516 if (body.size() > 0) {
1517 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001518 setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001519 true /* with signature */);
1520 } catch (UnsupportedEncodingException e) {
1521 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1522 }
1523 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001524 }
1525
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001526 @VisibleForTesting
1527 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001528 addAttachments(refMessage.getAttachments());
1529 }
1530
1531 public long addAttachments(List<Attachment> attachments) {
1532 long size = 0;
1533 AttachmentFailureException error = null;
1534 for (Attachment a : attachments) {
1535 try {
1536 size += mAttachmentsView.addAttachment(mAccount, a);
1537 } catch (AttachmentFailureException e) {
1538 error = e;
1539 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001540 }
Mark Wei434f2942012-08-24 11:54:02 -07001541 if (error != null) {
1542 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1543 if (attachments.size() > 1) {
1544 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1545 } else {
1546 showAttachmentTooBigToast(error.getErrorRes());
1547 }
1548 }
1549 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001550 }
1551
1552 /**
1553 * When an attachment is too large to be added to a message, show a toast.
1554 * This method also updates the position of the toast so that it is shown
1555 * clearly above they keyboard if it happens to be open.
1556 */
Mark Wei434f2942012-08-24 11:54:02 -07001557 private void showAttachmentTooBigToast(int errorRes) {
1558 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1559 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1560 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001561 }
1562
Mark Wei434f2942012-08-24 11:54:02 -07001563 private void showErrorToast(String message) {
1564 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1565 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001566 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1567 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1568 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001569 }
1570
Paul Westbrookf97588b2012-03-20 11:11:37 -07001571 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001572 Bundle extras = intent.getExtras();
1573 if (extras == null) {
1574 extras = Bundle.EMPTY;
1575 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001576 final String action = intent.getAction();
1577 if (!mAttachmentsChanged) {
1578 long totalSize = 0;
1579 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1580 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1581 for (String uriString : uris) {
1582 final Uri uri = Uri.parse(uriString);
1583 long size = 0;
1584 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001585 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1586 size = mAttachmentsView.addAttachment(mAccount, a);
1587
1588 Analytics.getInstance().sendEvent("send_intent_attachment",
1589 Utils.normalizeMimeType(a.getContentType()), null, size);
1590
Paul Westbrookf97588b2012-03-20 11:11:37 -07001591 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001592 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001593 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001594 }
1595 totalSize += size;
1596 }
1597 }
mindyp9a9e8d62012-10-03 12:24:07 -07001598 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1599 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1600 ArrayList<Parcelable> uris = extras
1601 .getParcelableArrayList(Intent.EXTRA_STREAM);
1602 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1603 for (Parcelable uri : uris) {
1604 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001605 final Attachment a = mAttachmentsView.generateLocalAttachment(
1606 (Uri) uri);
1607 attachments.add(a);
1608
1609 Analytics.getInstance().sendEvent("send_intent_attachment",
1610 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1611
mindyp9a9e8d62012-10-03 12:24:07 -07001612 } catch (AttachmentFailureException e) {
1613 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1614 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1615 getApplicationContext(),
1616 mAccount.settings.getMaxAttachmentSize());
1617 showErrorToast(getString
1618 (R.string.generic_attachment_problem, maxSize));
1619 }
1620 }
1621 totalSize += addAttachments(attachments);
1622 } else {
1623 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1624 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001625 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001626 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1627 size = mAttachmentsView.addAttachment(mAccount, a);
1628
1629 Analytics.getInstance().sendEvent("send_intent_attachment",
1630 Utils.normalizeMimeType(a.getContentType()), null, size);
1631
Paul Westbrookf97588b2012-03-20 11:11:37 -07001632 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001633 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001634 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001635 }
mindyp9a9e8d62012-10-03 12:24:07 -07001636 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001637 }
1638 }
1639
1640 if (totalSize > 0) {
1641 mAttachmentsChanged = true;
1642 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001643
1644 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1645 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001646 }
1647 }
1648 }
1649
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001650 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001651 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1652 mShowQuotedText = true;
1653 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001654
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001655 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1656 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001657 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1658 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001659 }
1660
1661 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001662 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001663 boolean ccVisible = mCcBccView.isCcVisible();
1664 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001665 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001666 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001667 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001668 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001669 : R.string.add_bcc_label));
1670 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001671 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001672 }
1673 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001674 }
1675
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001676 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001677 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1678 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1679 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1680 if (showCc || showBcc) {
1681 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001682 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001683 }
1684 }
1685
Mindy Pereira013194c2012-01-06 15:09:33 -08001686 /**
1687 * Add attachment and update the compose area appropriately.
1688 * @param data
1689 */
1690 public void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07001691 if (data == null) {
1692 return;
1693 }
1694
1695 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1696 final ClipData clipData = data.getClipData();
1697 if (clipData != null) {
1698 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1699 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1700 }
1701 return;
1702 }
1703 }
1704
1705 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07001706 }
1707
Andy Huang5c5fd572012-04-08 18:19:29 -07001708 public void addAttachmentAndUpdateView(Uri contentUri) {
1709 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001710 return;
1711 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001712 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001713 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1714 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001715 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001716 showErrorToast(getResources().getString(
1717 e.getErrorRes(),
1718 AttachmentUtils.convertToHumanReadableSize(
1719 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001720 }
1721 }
1722
1723 public void addAttachmentAndUpdateView(Attachment attachment) {
1724 try {
Mark Wei434f2942012-08-24 11:54:02 -07001725 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001726 if (size > 0) {
1727 mAttachmentsChanged = true;
1728 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001729 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001730 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001731 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001732 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001733 }
1734 }
1735
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001736 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001737 // Don't populate the address if this is a forward.
1738 if (action == ComposeActivity.FORWARD) {
1739 return;
1740 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001741 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001742 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001743
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001744 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1745 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001746 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001747 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001748 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001749 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001750 final String[] replyToAddresses = refMessage.getReplyToAddressesUnescaped();
Tony Mantler89de9eb2013-07-25 11:43:58 -07001751 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1752 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
1753
mindyp65b06f52012-11-21 10:35:08 -08001754 // If there is no reply to address, the reply to address is the sender.
Tony Mantler24f116f2014-01-16 10:20:50 -08001755 boolean hasReplyTo = false;
1756 for (final String replyToAddress : replyToAddresses) {
1757 if (!TextUtils.isEmpty(replyToAddress)) {
1758 hasReplyTo = true;
1759 }
1760 }
1761 if (!hasReplyTo) {
1762 replyToAddresses[0] = fromAddress;
mindyp65b06f52012-11-21 10:35:08 -08001763 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001764
1765 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1766 // Cc list is the union of the To and Cc recipients of the original
1767 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001768 // already on the To list.
1769 if (action == ComposeActivity.REPLY) {
Tony Mantler24f116f2014-01-16 10:20:50 -08001770 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001771 addToAddresses(toAddresses);
1772 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001773 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08001774 toAddresses = initToRecipients(fromAddress, replyToAddresses, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001775 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001776 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001777 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001778 addCcAddresses(ccAddresses, toAddresses);
1779 }
1780 }
1781
1782 private void addToAddresses(Collection<String> addresses) {
1783 addAddressesToList(addresses, mTo);
1784 }
1785
1786 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001787 addCcAddressesToList(tokenizeAddressList(addresses),
1788 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001789 }
1790
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001791 private void addBccAddresses(Collection<String> addresses) {
1792 addAddressesToList(addresses, mBcc);
1793 }
1794
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001795 @VisibleForTesting
1796 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1797 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1798 String address;
1799
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001800 if (compareToList == null) {
1801 for (Rfc822Token[] tokens : addresses) {
1802 for (int i = 0; i < tokens.length; i++) {
1803 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001804 list.append(address + END_TOKEN);
1805 }
1806 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001807 } else {
1808 HashSet<String> compareTo = convertToHashSet(compareToList);
1809 for (Rfc822Token[] tokens : addresses) {
1810 for (int i = 0; i < tokens.length; i++) {
1811 address = tokens[i].toString();
1812 // Check if this is a duplicate:
1813 if (!compareTo.contains(tokens[i].getAddress())) {
1814 // Get the address here
1815 list.append(address + END_TOKEN);
1816 }
1817 }
1818 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001819 }
1820 }
1821
Scott Kennedyff8553f2013-04-05 20:57:44 -07001822 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
1823 final HashSet<String> hash = new HashSet<String>();
1824 for (final Rfc822Token[] tokens : list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001825 for (int i = 0; i < tokens.length; i++) {
1826 hash.add(tokens[i].getAddress());
1827 }
1828 }
1829 return hash;
1830 }
1831
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001832 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1833 @VisibleForTesting
1834 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1835
1836 for (String address: addresses) {
1837 tokenized.add(Rfc822Tokenizer.tokenize(address));
1838 }
1839 return tokenized;
1840 }
1841
1842 @VisibleForTesting
1843 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1844 for (String address : addresses) {
1845 addAddressToList(address, list);
1846 }
1847 }
1848
Scott Kennedyff8553f2013-04-05 20:57:44 -07001849 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001850 if (address == null || list == null)
1851 return;
1852
Scott Kennedyff8553f2013-04-05 20:57:44 -07001853 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001854
1855 for (int i = 0; i < tokens.length; i++) {
1856 list.append(tokens[i] + END_TOKEN);
1857 }
1858 }
1859
1860 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001861 protected Collection<String> initToRecipients(final String fullSenderAddress,
Tony Mantler24f116f2014-01-16 10:20:50 -08001862 final String[] replyToAddresses, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001863 // The To recipient is the reply-to address specified in the original
1864 // message, unless it is:
1865 // the current user OR a custom from of the current user, in which case
1866 // it's the To recipient list of the original message.
1867 // OR missing, in which case use the sender of the original message
1868 Set<String> toAddresses = Sets.newHashSet();
Tony Mantler24f116f2014-01-16 10:20:50 -08001869 for (final String replyToAddress : replyToAddresses) {
1870 if (!TextUtils.isEmpty(replyToAddress)
1871 && !recipientMatchesThisAccount(replyToAddress)) {
1872 toAddresses.add(replyToAddress);
1873 }
1874 }
1875 if (toAddresses.size() == 0) {
mindyp65b06f52012-11-21 10:35:08 -08001876 // In this case, the user is replying to a message in which their
Tony Mantler24f116f2014-01-16 10:20:50 -08001877 // current account or some of their custom from addresses are the only
1878 // recipients and they sent the original message.
mindyp65b06f52012-11-21 10:35:08 -08001879 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
1880 && recipientMatchesThisAccount(inToAddresses[0])) {
1881 toAddresses.add(inToAddresses[0]);
1882 return toAddresses;
1883 }
1884 // This happens if the user replies to a message they originally
1885 // wrote. In this case, "reply" really means "re-send," so we
1886 // target the original recipients. This works as expected even
1887 // if the user sent the original message to themselves.
1888 for (String address : inToAddresses) {
1889 if (!recipientMatchesThisAccount(address)) {
1890 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08001891 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001892 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001893 }
1894 return toAddresses;
1895 }
1896
Scott Kennedyff8553f2013-04-05 20:57:44 -07001897 private void addRecipients(final Set<String> recipients, final String[] addresses) {
1898 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001899 // Do not add this account, or any of its custom from addresses, to
1900 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001901 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08001902 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001903 recipients.add(email.replace("\"\"", ""));
1904 }
1905 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001906 }
1907
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001908 /**
1909 * A recipient matches this account if it has the same address as the
1910 * currently selected account OR one of the custom from addresses associated
1911 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001912 * @param recipientAddress address we are comparing with the currently selected account
1913 * @return
1914 */
mindyp5ee5d692012-11-19 16:02:16 -08001915 protected boolean recipientMatchesThisAccount(String recipientAddress) {
1916 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08001917 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001918 }
1919
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001920 /**
1921 * Returns a formatted subject string with the appropriate prefix for the action type.
1922 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
1923 */
Tony Mantlera954f992013-12-03 11:22:56 -08001924 public static String buildFormattedSubject(final Resources res, final String subject,
1925 final int action) {
1926 final String prefix;
1927 final String correctedSubject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001928 if (action == ComposeActivity.COMPOSE) {
1929 prefix = "";
1930 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001931 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001932 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001933 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001934 }
1935
1936 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07001937 if (!TextUtils.isEmpty(subject)
1938 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001939 correctedSubject = subject;
1940 } else {
Tony Mantlera954f992013-12-03 11:22:56 -08001941 final String subjectOrNoSubject = TextUtils.isEmpty(subject) ?
1942 res.getString(R.string.no_subject) :
1943 subject;
1944
1945 correctedSubject =
1946 res.getString(R.string.formatted_subject, prefix, subjectOrNoSubject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001947 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001948
1949 return correctedSubject;
1950 }
1951
1952 private void setSubject(Message refMessage, int action) {
1953 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001954 }
1955
Mindy Pereira818143e2012-01-11 13:59:49 -08001956 private void initRecipients() {
1957 setupRecipients(mTo);
1958 setupRecipients(mCc);
1959 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001960 }
1961
Mindy Pereira818143e2012-01-11 13:59:49 -08001962 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001963 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001964 if (mValidator == null) {
Tony Mantler79b11562013-10-09 15:31:50 -07001965 final String accountName = mAccount.getEmailAddress();
Mindy Pereira33fe9082012-01-09 16:24:30 -08001966 int offset = accountName.indexOf("@") + 1;
1967 String account = accountName;
Tony Mantler79b11562013-10-09 15:31:50 -07001968 if (offset > 0) {
1969 account = account.substring(offset);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001970 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001971 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001972 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001973 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001974 }
1975
1976 @Override
1977 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001978 final int id = v.getId();
1979 if (id == R.id.add_cc_bcc) {
1980 // Verify that cc/ bcc aren't showing.
1981 // Animate in cc/bcc.
1982 showCcBccViews();
Andrew Sapperstein6aea7862013-10-24 19:59:51 -07001983 } else if (id == R.id.add_attachment) {
1984 doAttach(Utils.isRunningKitkatOrLater() ? MIME_TYPE_ALL : MIME_TYPE_PHOTO);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001985 }
1986 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001987
1988 @Override
1989 public boolean onCreateOptionsMenu(Menu menu) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07001990 final boolean superCreated = super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07001991 // Don't render any menu items when there are no accounts.
1992 if (mAccounts == null || mAccounts.length == 0) {
Tony Mantler5b8799a2013-10-31 10:43:03 -07001993 return superCreated;
Mindy Pereirab199d172012-08-13 11:04:03 -07001994 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001995 MenuInflater inflater = getMenuInflater();
1996 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08001997
1998 /*
1999 * Start save in the correct enabled state.
2000 * 1) If a user launches compose from within gmail, save is disabled
2001 * until they add something, at which point, save is enabled, auto save
2002 * on exit; if the user empties everything, save is disabled, exiting does not
2003 * auto-save
2004 * 2) if a user replies/ reply all/ forwards from within gmail, save is
2005 * disabled until they change something, at which point, save is
2006 * enabled, auto save on exit; if the user empties everything, save is
2007 * disabled, exiting does not auto-save.
2008 * 3) If a user launches compose from another application and something
2009 * gets populated (attachments, recipients, body, subject, etc), save is
2010 * enabled, auto save on exit; if the user empties everything, save is
2011 * disabled, exiting does not auto-save
2012 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08002013 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08002014 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07002015 enableSave(mInnerSavedState != null ?
2016 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08002017 : (Intent.ACTION_SEND.equals(action)
2018 || Intent.ACTION_SEND_MULTIPLE.equals(action)
2019 || Intent.ACTION_SENDTO.equals(action)
2020 || shouldSave()));
2021
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07002022 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2023 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2024 if (helpItem != null) {
2025 helpItem.setVisible(mAccount != null
2026 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2027 }
2028 if (sendFeedbackItem != null) {
2029 sendFeedbackItem.setVisible(mAccount != null
2030 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2031 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002032
Andrew Sapperstein8809f9f2013-10-11 16:13:35 -07002033 // Show attach picture on pre-K devices.
2034 menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002035
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002036 return true;
2037 }
2038
2039 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002040 public boolean onPrepareOptionsMenu(Menu menu) {
2041 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08002042 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002043 // Its possible there is a menu item OR a button.
2044 boolean ccFieldVisible = mCc.isShown();
2045 boolean bccFieldVisible = mBcc.isShown();
2046 if (!ccFieldVisible || !bccFieldVisible) {
2047 ccBcc.setVisible(true);
2048 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
2049 : R.string.add_bcc_label));
2050 } else {
2051 ccBcc.setVisible(false);
2052 }
2053 }
2054 return true;
2055 }
2056
2057 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002058 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002059 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002060
2061 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id, null, 0);
2062
Mindy Pereira75f66632012-01-11 11:42:02 -08002063 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002064 if (id == R.id.add_file_attachment) {
2065 doAttach(MIME_TYPE_ALL);
2066 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002067 doAttach(MIME_TYPE_PHOTO);
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002068 } else if (id == R.id.add_cc_bcc) {
2069 showCcBccViews();
2070 } else if (id == R.id.save) {
2071 doSave(true);
2072 } else if (id == R.id.send) {
2073 doSend();
2074 } else if (id == R.id.discard) {
2075 doDiscard();
2076 } else if (id == R.id.settings) {
2077 Utils.showSettings(this, mAccount);
2078 } else if (id == android.R.id.home) {
2079 onAppUpPressed();
2080 } else if (id == R.id.help_info_menu_item) {
2081 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2082 } else if (id == R.id.feedback_menu_item) {
2083 Utils.sendFeedback(this, mAccount, false);
2084 } else {
2085 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002086 }
2087 return !handled ? super.onOptionsItemSelected(item) : handled;
2088 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002089
Mindy Pereirab199d172012-08-13 11:04:03 -07002090 @Override
2091 public void onBackPressed() {
2092 // If we are showing the wait fragment, just exit.
2093 if (getWaitFragment() != null) {
2094 finish();
2095 } else {
2096 super.onBackPressed();
2097 }
2098 }
2099
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002100 /**
2101 * Carries out the "up" action in the action bar.
2102 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002103 private void onAppUpPressed() {
2104 if (mLaunchedFromEmail) {
2105 // If this was started from Gmail, simply treat app up as the system back button, so
2106 // that the last view is restored.
2107 onBackPressed();
2108 return;
2109 }
2110
2111 // Fire the main activity to ensure it launches the "top" screen of mail.
2112 // Since the main Activity is singleTask, it should revive that task if it was already
2113 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002114 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002115 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2116 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2117 startActivity(mailIntent);
2118 finish();
2119 }
2120
Mindy Pereira33fe9082012-01-09 16:24:30 -08002121 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002122 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002123 logSendOrSave(false /* save */);
2124 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002125 }
2126
Mindy Pereira48e31b02012-05-30 13:12:24 -07002127 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002128 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002129 }
2130
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002131 @VisibleForTesting
2132 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002133 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002134 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2135 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002136 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2137 }
2138
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002139 @VisibleForTesting
2140 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002141 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002142 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002143 public final SendOrSaveCallback mSendOrSaveCallback;
2144 @VisibleForTesting
2145 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002146 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002147
2148 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002149 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002150 mContext = context;
2151 mSendOrSaveCallback = callback;
2152 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002153 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002154 }
2155
2156 @Override
2157 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002158 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002159
Mindy Pereira92551d02012-04-05 11:31:12 -07002160 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002161 Message message = mSendOrSaveCallback.getMessage();
2162 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002163 // If a previous draft has been saved, in an account that is different
2164 // than what the user wants to send from, remove the old draft, and treat this
2165 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002166 if (mExistingDraftAccount != null
2167 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002168 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2169 ContentResolver resolver = mContext.getContentResolver();
2170 ContentValues values = new ContentValues();
2171 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002172 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2173 new ContentProviderTask.UpdateTask()
2174 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2175 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002176 } else {
2177 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002178 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002179 // reset messageId to 0, so a new message will be created
2180 messageId = UIProvider.INVALID_MESSAGE_ID;
2181 }
2182 }
2183
2184 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002185 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002186
2187 if (!sendOrSaveMessage.mSave) {
Tony Mantler9f324232013-08-08 14:24:30 -07002188 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002189 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Tony Mantler9f324232013-08-08 14:24:30 -07002190 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002191 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Tony Mantler9f324232013-08-08 14:24:30 -07002192 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002193 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2194 }
2195 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2196 }
2197
Tony Mantler9f324232013-08-08 14:24:30 -07002198 private static void incrementRecipientsTimesContacted(final Context context,
2199 final String addressString) {
2200 if (TextUtils.isEmpty(addressString)) {
2201 return;
2202 }
2203 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2204 final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
2205 for (int i = 0; i < tokens.length;i++) {
2206 recipients.add(tokens[i].getAddress());
2207 }
2208 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
2209 statsUpdater.updateWithAddress(recipients);
2210 }
2211
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002212 /**
2213 * Send or Save a message.
2214 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002215 private void sendOrSaveMessage(final long messageIdToSave,
2216 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002217 final ContentResolver resolver = mContext.getContentResolver();
2218 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2219
2220 final String accountMethod = sendOrSaveMessage.mSave ?
2221 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2222 UIProvider.AccountCallMethods.SEND_MESSAGE;
2223
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002224 try {
2225 if (updateExistingMessage) {
2226 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002227
Paul Westbrook013a23c2013-02-22 10:37:41 -08002228 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002229 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002230 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002231 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002232 final Bundle result = callAccountSendSaveMethod(resolver,
2233 selectedAccount.account, accountMethod, sendOrSaveMessage);
2234 if (result != null) {
2235 // If a non-null value was returned, then the provider handled the call
2236 // method
2237 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002238 }
2239 if (sendOrSaveMessage.mSave && messageUri != null) {
2240 final Cursor messageCursor = resolver.query(messageUri,
2241 UIProvider.MESSAGE_PROJECTION, null, null, null);
2242 if (messageCursor != null) {
2243 try {
2244 if (messageCursor.moveToFirst()) {
2245 // Broadcast notification that a new message has
2246 // been allocated
2247 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2248 new Message(messageCursor));
2249 }
2250 } finally {
2251 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002252 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002253 }
2254 }
2255 }
2256 } finally {
2257 // Close any opened file descriptors
2258 closeOpenedAttachmentFds(sendOrSaveMessage);
2259 }
2260 }
2261
Scott Kennedyff8553f2013-04-05 20:57:44 -07002262 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002263 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2264 if (openedFds != null) {
2265 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002266 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002267 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2268 if (fd != null) {
2269 try {
2270 fd.close();
2271 } catch (IOException e) {
2272 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002273 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002274 }
2275 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002276 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002277 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002278
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002279 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002280 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002281 *
2282 * If this was successful, this method will return an non-null Bundle instance
2283 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002284 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2285 final Account account, final String method,
2286 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002287 // Copy all of the values from the content values to the bundle
2288 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2289 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2290
2291 for (Entry<String, Object> entry : valueSet) {
2292 final Object entryValue = entry.getValue();
2293 final String key = entry.getKey();
2294 if (entryValue instanceof String) {
2295 methodExtras.putString(key, (String)entryValue);
2296 } else if (entryValue instanceof Boolean) {
2297 methodExtras.putBoolean(key, (Boolean)entryValue);
2298 } else if (entryValue instanceof Integer) {
2299 methodExtras.putInt(key, (Integer)entryValue);
2300 } else if (entryValue instanceof Long) {
2301 methodExtras.putLong(key, (Long)entryValue);
2302 } else {
2303 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2304 entryValue.getClass().getName());
2305 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002306 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002307
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002308 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2309 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2310 if (fdMap != null) {
2311 methodExtras.putParcelable(
2312 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2313 }
2314
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002315 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002316 }
2317 }
2318
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002319 @VisibleForTesting
2320 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002321 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002322 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002323 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002324 @VisibleForTesting
2325 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002326 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002327 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002328
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002329 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2330 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002331 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002332 mValues = values;
2333 mRefMessageId = refMessageId;
2334 mSave = save;
2335 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002336
2337 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002338 }
2339
2340 int requestId() {
2341 return mRequestId;
2342 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002343
2344 Bundle attachmentFds() {
2345 return mAttachmentFds;
2346 }
2347
2348 /**
2349 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2350 * called before the ComposeActivity finishes.
2351 * Note: The caller is responsible for closing these file descriptors.
2352 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002353 private static Bundle initializeAttachmentFds(final Context context,
2354 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002355 if (attachments == null || attachments.size() == 0) {
2356 return null;
2357 }
2358
2359 final Bundle result = new Bundle(attachments.size());
2360 final ContentResolver resolver = context.getContentResolver();
2361
2362 for (Attachment attachment : attachments) {
2363 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2364 continue;
2365 }
2366
2367 ParcelFileDescriptor fileDescriptor;
2368 try {
2369 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2370 } catch (FileNotFoundException e) {
2371 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2372 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002373 } catch (SecurityException e) {
2374 // We have encountered a security exception when attempting to open the file
2375 // specified by the content uri. If the attachment has been cached, this
2376 // isn't a problem, as even through the original permission may have been
2377 // revoked, we have cached the file. This will happen when saving/sending
2378 // a previously saved draft.
2379 // TODO(markwei): Expose whether the attachment has been cached through the
2380 // attachment object. This would allow us to limit when the log is made, as
2381 // if the attachment has been cached, this really isn't an error
2382 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2383 // Just set the file descriptor to null, as the underlying provider needs
2384 // to handle the file descriptor not being set.
2385 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002386 }
2387
2388 if (fileDescriptor != null) {
2389 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2390 }
2391 }
2392
2393 return result;
2394 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002395 }
2396
2397 /**
2398 * Get the to recipients.
2399 */
2400 public String[] getToAddresses() {
2401 return getAddressesFromList(mTo);
2402 }
2403
2404 /**
2405 * Get the cc recipients.
2406 */
2407 public String[] getCcAddresses() {
2408 return getAddressesFromList(mCc);
2409 }
2410
2411 /**
2412 * Get the bcc recipients.
2413 */
2414 public String[] getBccAddresses() {
2415 return getAddressesFromList(mBcc);
2416 }
2417
2418 public String[] getAddressesFromList(RecipientEditTextView list) {
2419 if (list == null) {
2420 return new String[0];
2421 }
2422 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2423 int count = tokens.length;
2424 String[] result = new String[count];
2425 for (int i = 0; i < count; i++) {
2426 result[i] = tokens[i].toString();
2427 }
2428 return result;
2429 }
2430
2431 /**
2432 * Check for invalid email addresses.
2433 * @param to String array of email addresses to check.
2434 * @param wrongEmailsOut Emails addresses that were invalid.
2435 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002436 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002437 if (mValidator == null) {
2438 return;
2439 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002440 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002441 if (!mValidator.isValid(email)) {
2442 wrongEmailsOut.add(email);
2443 }
2444 }
2445 }
2446
Tony Mantler2558b502013-07-09 10:53:34 -07002447 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002448 // Public no-args constructor needed for fragment re-instantiation
2449 public RecipientErrorDialogFragment() {}
2450
Tony Mantler2558b502013-07-09 10:53:34 -07002451 public static RecipientErrorDialogFragment newInstance(final String message) {
2452 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2453 final Bundle args = new Bundle(1);
2454 args.putString("message", message);
2455 frag.setArguments(args);
2456 return frag;
2457 }
2458
2459 @Override
2460 public Dialog onCreateDialog(Bundle savedInstanceState) {
2461 final String message = getArguments().getString("message");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002462 return new AlertDialog.Builder(getActivity())
2463 .setMessage(message)
Tony Mantler2558b502013-07-09 10:53:34 -07002464 .setPositiveButton(
2465 R.string.ok, new Dialog.OnClickListener() {
2466 @Override
2467 public void onClick(DialogInterface dialog, int which) {
2468 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2469 }
2470 }).create();
2471 }
2472 }
2473
2474 private void finishRecipientErrorDialog() {
2475 // after the user dismisses the recipient error
2476 // dialog we want to make sure to refocus the
2477 // recipient to field so they can fix the issue
2478 // easily
2479 if (mTo != null) {
2480 mTo.requestFocus();
2481 }
2482 }
2483
Mindy Pereira82cc5662012-01-09 17:29:30 -08002484 /**
2485 * Show an error because the user has entered an invalid recipient.
2486 * @param message
2487 */
Tony Mantler2558b502013-07-09 10:53:34 -07002488 private void showRecipientErrorDialog(final String message) {
2489 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2490 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002491 }
2492
2493 /**
2494 * Update the state of the UI based on whether or not the current draft
2495 * needs to be saved and the message is not empty.
2496 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002497 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002498 if (mSave != null) {
2499 mSave.setEnabled((shouldSave() && !isBlank()));
2500 }
2501 }
2502
2503 /**
2504 * Returns true if we need to save the current draft.
2505 */
2506 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002507 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002508 // The message should only be saved if:
2509 // It hasn't been sent AND
2510 // Some text has been added to the message OR
2511 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002512 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002513 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002514 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002515 }
2516 }
2517
2518 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002519 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002520 * @return boolean
2521 */
2522 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002523 // Need to check for null since isBlank() can be called from onPause()
2524 // before findViews() is called
2525 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2526 mAttachmentsView == null) {
2527 LogUtils.w(LOG_TAG, "null views in isBlank check");
2528 return true;
2529 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002530 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002531 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2532 mBodyView.getText().toString()) == 0)
2533 && mTo.length() == 0
2534 && mCc.length() == 0 && mBcc.length() == 0
2535 && mAttachmentsView.getAttachments().size() == 0;
2536 }
2537
2538 @VisibleForTesting
2539 protected int getSignatureStartPosition(String signature, String bodyText) {
2540 int startPos = -1;
2541
2542 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2543 return startPos;
2544 }
2545
2546 int bodyLength = bodyText.length();
2547 int signatureLength = signature.length();
2548 String printableVersion = convertToPrintableSignature(signature);
2549 int printableLength = printableVersion.length();
2550
2551 if (bodyLength >= printableLength
2552 && bodyText.substring(bodyLength - printableLength)
2553 .equals(printableVersion)) {
2554 startPos = bodyLength - printableLength;
2555 } else if (bodyLength >= signatureLength
2556 && bodyText.substring(bodyLength - signatureLength)
2557 .equals(signature)) {
2558 startPos = bodyLength - signatureLength;
2559 }
2560 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002561 }
2562
2563 /**
2564 * Allows any changes made by the user to be ignored. Called when the user
2565 * decides to discard a draft.
2566 */
2567 private void discardChanges() {
2568 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002569 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002570 mReplyFromChanged = false;
2571 }
2572
2573 /**
Mindy Pereira181df782012-03-01 13:32:44 -08002574 * @param save
2575 * @param showToast
2576 * @return Whether the send or save succeeded.
2577 */
2578 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002579 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002580 if (mAccounts == null || mAccount == null) {
2581 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002582 if (autoSend) {
2583 finish();
2584 }
Mark Wei009b3712012-10-18 18:07:50 -07002585 return false;
2586 }
2587
Scott Kennedyff8553f2013-04-05 20:57:44 -07002588 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002589 if (orientationChanged) {
2590 to = cc = bcc = new String[0];
2591 } else {
2592 to = getToAddresses();
2593 cc = getCcAddresses();
2594 bcc = getBccAddresses();
2595 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002596
Mindy Pereira181df782012-03-01 13:32:44 -08002597 // Don't let the user send to nobody (but it's okay to save a message
2598 // with no recipients)
2599 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2600 showRecipientErrorDialog(getString(R.string.recipient_needed));
2601 return false;
2602 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002603
Mindy Pereira181df782012-03-01 13:32:44 -08002604 List<String> wrongEmails = new ArrayList<String>();
2605 if (!save) {
2606 checkInvalidEmails(to, wrongEmails);
2607 checkInvalidEmails(cc, wrongEmails);
2608 checkInvalidEmails(bcc, wrongEmails);
2609 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002610
Mindy Pereira181df782012-03-01 13:32:44 -08002611 // Don't let the user send an email with invalid recipients
2612 if (wrongEmails.size() > 0) {
2613 String errorText = String.format(getString(R.string.invalid_recipient),
2614 wrongEmails.get(0));
2615 showRecipientErrorDialog(errorText);
2616 return false;
2617 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002618
Mindy Pereira181df782012-03-01 13:32:44 -08002619 // Show a warning before sending only if there are no attachments.
2620 if (!save) {
2621 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2622 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002623 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002624
Mindy Pereira181df782012-03-01 13:32:44 -08002625 // A warning about an empty body may not be warranted when
2626 // forwarding mails, since a common use case is to forward
2627 // quoted text and not append any more text.
2628 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002629
Mindy Pereira181df782012-03-01 13:32:44 -08002630 // When we bring up a dialog warning the user about a send,
2631 // assume that they accept sending the message. If they do not,
2632 // the dialog listener is required to enable sending again.
2633 if (warnAboutEmptySubject) {
Tony Mantler2558b502013-07-09 10:53:34 -07002634 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, save,
2635 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002636 return true;
2637 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002638
Mindy Pereira181df782012-03-01 13:32:44 -08002639 if (warnAboutEmptyBody) {
Tony Mantler2558b502013-07-09 10:53:34 -07002640 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, save,
2641 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002642 return true;
2643 }
2644 }
2645 // Ask for confirmation to send (if always required)
2646 if (showSendConfirmation()) {
Tony Mantler2558b502013-07-09 10:53:34 -07002647 showSendConfirmDialog(R.string.confirm_send_message, save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002648 return true;
2649 }
2650 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002651
Tony Mantler2558b502013-07-09 10:53:34 -07002652 sendOrSave(save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002653 return true;
2654 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002655
Mindy Pereira181df782012-03-01 13:32:44 -08002656 /**
2657 * Returns a boolean indicating whether warnings should be shown for empty
2658 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002659 *
Mindy Pereira181df782012-03-01 13:32:44 -08002660 * @return True if a warning should be shown for empty text fields
2661 */
2662 protected boolean showEmptyTextWarnings() {
2663 return mAttachmentsView.getAttachments().size() == 0;
2664 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002665
Mindy Pereira181df782012-03-01 13:32:44 -08002666 /**
2667 * Returns a boolean indicating whether the user should confirm each send
2668 *
2669 * @return True if a warning should be on each send
2670 */
2671 protected boolean showSendConfirmation() {
2672 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2673 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002674
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002675 public static class SendConfirmDialogFragment extends DialogFragment
2676 implements DialogInterface.OnClickListener {
2677
2678 private boolean mSave;
2679 private boolean mShowToast;
2680
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002681 // Public no-args constructor needed for fragment re-instantiation
2682 public SendConfirmDialogFragment() {}
2683
Tony Mantler2558b502013-07-09 10:53:34 -07002684 public static SendConfirmDialogFragment newInstance(final int messageId,
2685 final boolean save, final boolean showToast) {
2686 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2687 final Bundle args = new Bundle(3);
2688 args.putInt("messageId", messageId);
2689 args.putBoolean("save", save);
2690 args.putBoolean("showToast", showToast);
2691 frag.setArguments(args);
2692 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002693 }
Tony Mantler2558b502013-07-09 10:53:34 -07002694
2695 @Override
2696 public Dialog onCreateDialog(Bundle savedInstanceState) {
2697 final int messageId = getArguments().getInt("messageId");
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002698 mSave = getArguments().getBoolean("save");
2699 mShowToast = getArguments().getBoolean("showToast");
2700
2701 final int confirmTextId = (messageId == R.string.confirm_send_message) ?
2702 R.string.ok : R.string.send;
Tony Mantler2558b502013-07-09 10:53:34 -07002703
2704 return new AlertDialog.Builder(getActivity())
2705 .setMessage(messageId)
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002706 .setPositiveButton(confirmTextId, this)
Paul Westbrook7d1c5c42013-10-01 23:40:04 -07002707 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07002708 .create();
2709 }
Andrew Sapperstein530ac7a2013-10-29 19:12:17 -07002710
2711 @Override
2712 public void onClick(DialogInterface dialog, int which) {
2713 if (which == DialogInterface.BUTTON_POSITIVE) {
2714 ((ComposeActivity) getActivity()).finishSendConfirmDialog(mSave, mShowToast);
2715 }
2716 }
Tony Mantler2558b502013-07-09 10:53:34 -07002717 }
2718
2719 private void finishSendConfirmDialog(final boolean save, final boolean showToast) {
2720 sendOrSave(save, showToast);
2721 }
2722
2723 private void showSendConfirmDialog(final int messageId, final boolean save,
2724 final boolean showToast) {
2725 final DialogFragment frag = SendConfirmDialogFragment.newInstance(messageId, save,
2726 showToast);
2727 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08002728 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002729
Mindy Pereira181df782012-03-01 13:32:44 -08002730 /**
2731 * Returns whether the ComposeArea believes there is any text in the body of
2732 * the composition. TODO: When ComposeArea controls the Body as well, add
2733 * that here.
2734 */
2735 public boolean isBodyEmpty() {
2736 return !mQuotedTextView.isTextIncluded();
2737 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002738
Mindy Pereira181df782012-03-01 13:32:44 -08002739 /**
2740 * Test to see if the subject is empty.
2741 *
2742 * @return boolean.
2743 */
2744 // TODO: this will likely go away when composeArea.focus() is implemented
2745 // after all the widget control is moved over.
2746 public boolean isSubjectEmpty() {
2747 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2748 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002749
Andy Huang0a2a3462013-12-20 15:56:13 -08002750 @VisibleForTesting
2751 public String getSubject() {
2752 return mSubject.getText().toString();
2753 }
2754
Mindy Pereira181df782012-03-01 13:32:44 -08002755 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002756 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07002757 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08002758 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07002759 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08002760 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002761
Paul Westbrookb4931c62013-01-14 17:51:18 -08002762 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07002763
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002764 MessageModification.putToAddresses(values, message.getToAddresses());
2765 MessageModification.putCcAddresses(values, message.getCcAddresses());
2766 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002767
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002768 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07002769
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002770 MessageModification.putSubject(values, message.subject);
Paul Westbrookb4931c62013-01-14 17:51:18 -08002771 // Make sure to remove only the composing spans from the Spannable before saving.
2772 final String htmlBody = Html.toHtml(removeComposingSpans(body));
Paul Westbrook05b92b82012-04-20 13:29:37 -07002773
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002774 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2775 StringBuilder fullBody = new StringBuilder(htmlBody);
2776 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002777 // HTML gets converted to text for now
2778 final String text = quotedText.toString();
2779 if (QuotedTextView.containsQuotedText(text)) {
2780 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07002781 final int quoteStartPos = fullBody.length() + pos;
2782 fullBody.append(text);
2783 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07002784 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07002785 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002786 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002787 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2788 // This shouldn't happen, but just use what we have,
2789 // and don't do server-side expansion
2790 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002791 }
2792 }
Mindy Pereira002ff522012-05-30 10:31:26 -07002793 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07002794 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002795 if (refMessage != null) {
2796 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2797 MessageModification.putBodyHtml(values, fullBody.toString());
2798 }
2799 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08002800 MessageModification.putBody(values,
2801 Utils.convertHtmlToPlainText(fullBody.toString()).toString());
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002802 }
2803 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002804 MessageModification.putBodyHtml(values, fullBody.toString());
mindypc59dd822012-11-13 10:56:21 -08002805 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2806 .toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002807 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002808 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002809 if (!TextUtils.isEmpty(refMessageId)) {
2810 MessageModification.putRefMessageId(values, refMessageId);
2811 }
Scott Kennedy60847252013-08-15 15:55:42 -07002812 if (extraValues != null) {
2813 values.putAll(extraValues);
2814 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002815 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2816 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08002817 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2818 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002819
Mindy Pereira181df782012-03-01 13:32:44 -08002820 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08002821 // Do the send/save action on the specified handler to avoid possible
2822 // ANRs
2823 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002824
Mindy Pereira181df782012-03-01 13:32:44 -08002825 return sendOrSaveMessage.requestId();
2826 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002827
Paul Westbrookb4931c62013-01-14 17:51:18 -08002828 /**
2829 * Removes any composing spans from the specified string. This will create a new
2830 * SpannableString instance, as to not modify the behavior of the EditText view.
2831 */
2832 private static SpannableString removeComposingSpans(Spanned body) {
2833 final SpannableString messageBody = new SpannableString(body);
2834 BaseInputConnection.removeComposingSpans(messageBody);
2835 return messageBody;
2836 }
2837
Mindy Pereira002ff522012-05-30 10:31:26 -07002838 private static int getDraftType(int mode) {
2839 int draftType = -1;
2840 switch (mode) {
2841 case ComposeActivity.COMPOSE:
2842 draftType = DraftType.COMPOSE;
2843 break;
2844 case ComposeActivity.REPLY:
2845 draftType = DraftType.REPLY;
2846 break;
2847 case ComposeActivity.REPLY_ALL:
2848 draftType = DraftType.REPLY_ALL;
2849 break;
2850 case ComposeActivity.FORWARD:
2851 draftType = DraftType.FORWARD;
2852 break;
2853 }
2854 return draftType;
2855 }
2856
Tony Mantler2558b502013-07-09 10:53:34 -07002857 private void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08002858 // Check if user is a monkey. Monkeys can compose and hit send
2859 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002860 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002861 return;
2862 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002863
Tony Mantler2558b502013-07-09 10:53:34 -07002864 final Spanned body = mBodyView.getEditableText();
2865
Mindy Pereira181df782012-03-01 13:32:44 -08002866 SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07002867 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08002868 private int mRestoredRequestId;
2869
Marc Blank0bbc8582012-04-23 15:07:57 -07002870 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002871 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002872 synchronized (mActiveTasks) {
2873 int numTasks = mActiveTasks.size();
2874 if (numTasks == 0) {
2875 // Start service so we won't be killed if this app is
2876 // put in the background.
2877 startService(new Intent(ComposeActivity.this, EmptyService.class));
2878 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002879
Mindy Pereira181df782012-03-01 13:32:44 -08002880 mActiveTasks.add(sendOrSaveTask);
2881 }
2882 if (sTestSendOrSaveCallback != null) {
2883 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2884 }
2885 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002886
Marc Blank0bbc8582012-04-23 15:07:57 -07002887 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002888 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2889 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002890 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08002891 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002892 mDraftId = message.id;
2893 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002894 if (sRequestMessageIdMap != null) {
2895 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2896 }
Mindy Pereira181df782012-03-01 13:32:44 -08002897 // Cache request message map, in case the process is killed
2898 saveRequestMap();
2899 }
2900 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002901 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002902 }
2903 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002904
Marc Blank0bbc8582012-04-23 15:07:57 -07002905 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002906 public Message getMessage() {
2907 synchronized (mDraftLock) {
2908 return mDraft;
2909 }
2910 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002911
Marc Blank0bbc8582012-04-23 15:07:57 -07002912 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002913 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07002914 // Update the last sent from account.
2915 if (mAccount != null) {
2916 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2917 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002918 if (success) {
2919 // Successfully sent or saved so reset change markers
2920 discardChanges();
2921 } else {
2922 // A failure happened with saving/sending the draft
2923 // TODO(pwestbro): add a better string that should be used
2924 // when failing to send or save
2925 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2926 .show();
2927 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002928
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002929 int numTasks;
2930 synchronized (mActiveTasks) {
2931 // Remove the task from the list of active tasks
2932 mActiveTasks.remove(task);
2933 numTasks = mActiveTasks.size();
2934 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002935
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002936 if (numTasks == 0) {
2937 // Stop service so we can be killed.
2938 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2939 }
2940 if (sTestSendOrSaveCallback != null) {
2941 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2942 }
2943 }
Mindy Pereira181df782012-03-01 13:32:44 -08002944 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002945
Tony Mantler1e05a1e2013-08-12 16:44:26 -07002946 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002947
Mindy Pereira181df782012-03-01 13:32:44 -08002948 if (mSendSaveTaskHandler == null) {
2949 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2950 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002951
Mindy Pereira181df782012-03-01 13:32:44 -08002952 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2953 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002954
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002955 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002956 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2957 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Scott Kennedy60847252013-08-15 15:55:42 -07002958 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount, mExtraValues);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002959
Mindy Pereira181df782012-03-01 13:32:44 -08002960 // Don't display the toast if the user is just changing the orientation,
2961 // but we still need to save the draft to the cursor because this is how we restore
2962 // the attachments when the configuration change completes.
2963 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2964 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2965 Toast.LENGTH_LONG).show();
2966 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002967
Mindy Pereira181df782012-03-01 13:32:44 -08002968 // Need to update variables here because the send or save completes
2969 // asynchronously even though the toast shows right away.
2970 discardChanges();
2971 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002972
Mindy Pereira181df782012-03-01 13:32:44 -08002973 // If we are sending, finish the activity
2974 if (!save) {
2975 finish();
2976 }
2977 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002978
Mindy Pereira181df782012-03-01 13:32:44 -08002979 /**
2980 * Save the state of the request messageid map. This allows for the Gmail
2981 * process to be killed, but and still allow for ComposeActivity instances
2982 * to be recreated correctly.
2983 */
2984 private void saveRequestMap() {
2985 // TODO: store the request map in user preferences.
2986 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002987
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002988 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002989 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2990 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002991 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07002992 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002993 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08002994 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002995 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2996 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002997 }
2998
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002999 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003000 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003001 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07003002 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08003003 }
3004 }
3005
Andy Huang4fe0af82013-08-20 17:24:51 -07003006 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003007 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07003008 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07003009 case COMPOSE:
3010 msgType = "new_message";
3011 break;
3012 case REPLY:
3013 msgType = "reply";
3014 break;
3015 case REPLY_ALL:
3016 msgType = "reply_all";
3017 break;
3018 case FORWARD:
3019 msgType = "forward";
3020 break;
3021 default:
3022 msgType = "unknown";
3023 break;
3024 }
Andy Huang4fe0af82013-08-20 17:24:51 -07003025 return msgType;
3026 }
3027
3028 private void logSendOrSave(boolean save) {
3029 if (!Analytics.isLoggable() || mAttachmentsView == null) {
3030 return;
3031 }
3032
3033 final String category = (save) ? "message_save" : "message_send";
3034 final int attachmentCount = getAttachments().size();
3035 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07003036 final String label;
3037 final long value;
3038 if (mComposeMode == COMPOSE) {
3039 label = Integer.toString(attachmentCount);
3040 value = attachmentCount;
3041 } else {
3042 label = null;
3043 value = 0;
3044 }
3045 Analytics.getInstance().sendEvent(category, msgType, label, value);
3046 }
3047
Mindy Pereira326c6602012-01-04 15:32:42 -08003048 @Override
3049 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003050 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003051 if (position == ComposeActivity.REPLY) {
3052 mComposeMode = ComposeActivity.REPLY;
3053 } else if (position == ComposeActivity.REPLY_ALL) {
3054 mComposeMode = ComposeActivity.REPLY_ALL;
3055 } else if (position == ComposeActivity.FORWARD) {
3056 mComposeMode = ComposeActivity.FORWARD;
3057 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003058 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003059 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003060 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003061 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003062 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003063 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003064 boolean showCc = false;
3065 boolean showBcc = false;
3066 if (mDraft != null) {
3067 // Following desktop behavior, if the user has added a BCC
3068 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003069 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003070 // Use the draft to determine what to populate.
3071 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003072 showCc = showBcc
3073 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003074 }
3075 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003076 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003077 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003078 }
3079 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003080 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003081 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003082 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003083 return true;
3084 }
3085
Mindy Pereirab3112a22012-06-20 12:10:03 -07003086 @VisibleForTesting
3087 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003088 // When switching between reply, reply all, forward,
3089 // follow the behavior of webview.
3090 // The contents of the following fields are cleared
3091 // so that they can be populated directly from the
3092 // ref message:
3093 // 1) Any recipient fields
3094 // 2) The subject
3095 mTo.setText("");
3096 mCc.setText("");
3097 mBcc.setText("");
3098 // Any edits to the subject are replaced with the original subject.
3099 mSubject.setText("");
3100
3101 // Any changes to the contents of the following fields are kept:
3102 // 1) Body
3103 // 2) Attachments
3104 // If the user made changes to attachments, keep their changes.
3105 if (!mAttachmentsChanged) {
3106 mAttachmentsView.deleteAllAttachments();
3107 }
3108 }
3109
Mindy Pereira326c6602012-01-04 15:32:42 -08003110 private class ComposeModeAdapter extends ArrayAdapter<String> {
3111
3112 private LayoutInflater mInflater;
3113
3114 public ComposeModeAdapter(Context context) {
3115 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3116 .getStringArray(R.array.compose_modes));
3117 }
3118
3119 private LayoutInflater getInflater() {
3120 if (mInflater == null) {
3121 mInflater = LayoutInflater.from(getContext());
3122 }
3123 return mInflater;
3124 }
3125
3126 @Override
3127 public View getView(int position, View convertView, ViewGroup parent) {
3128 if (convertView == null) {
3129 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3130 }
3131 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3132 return super.getView(position, convertView, parent);
3133 }
3134 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003135
3136 @Override
3137 public void onRespondInline(String text) {
3138 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003139 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003140 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003141 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003142 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003143 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003144 }
3145
3146 /**
3147 * Append text to the body of the message. If there is no existing body
3148 * text, just sets the body to text.
3149 *
3150 * @param text
3151 * @param withSignature True to append a signature.
3152 */
3153 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003154 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003155 if (bodyText != null && bodyText.length() > 0) {
3156 bodyText.append(text);
3157 } else {
3158 setBody(text, withSignature);
3159 }
3160 }
3161
3162 /**
3163 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003164 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003165 * @param text
3166 * @param withSignature True to append a signature.
3167 */
3168 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003169 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003170 if (withSignature) {
3171 appendSignature();
3172 }
3173 }
3174
3175 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003176 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07003177 boolean hasFocus = mBodyView.hasFocus();
mindyp27083062012-11-15 09:02:01 -08003178 int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
3179 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003180 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003181 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003182 // Appending a signature does not count as changing text.
3183 mBodyView.removeTextChangedListener(this);
3184 mBodyView.append(convertToPrintableSignature(mSignature));
3185 mBodyView.addTextChangedListener(this);
3186 }
Mindy Pereira433b1982012-04-03 11:53:07 -07003187 if (hasFocus) {
3188 focusBody();
3189 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003190 }
3191 }
3192
3193 private String convertToPrintableSignature(String signature) {
3194 String signatureResource = getResources().getString(R.string.signature);
3195 if (signature == null) {
3196 signature = "";
3197 }
3198 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003199 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003200
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003201 @Override
3202 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003203 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3204 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003205 // Clear a signature, if there was one.
3206 mBodyView.removeTextChangedListener(this);
3207 String oldSignature = mSignature;
3208 String bodyText = getBody().getText().toString();
3209 if (!TextUtils.isEmpty(oldSignature)) {
3210 int pos = getSignatureStartPosition(oldSignature, bodyText);
3211 if (pos > -1) {
3212 mBodyView.setText(bodyText.substring(0, pos));
3213 }
3214 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003215 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003216 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003217 // TODO: handle discarding attachments when switching accounts.
3218 // Only enable save for this draft if there is any other content
3219 // in the message.
3220 if (!isBlank()) {
3221 enableSave(true);
3222 }
3223 mReplyFromChanged = true;
3224 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003225 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003226 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003227
3228 public void enableSave(boolean enabled) {
3229 if (mSave != null) {
3230 mSave.setEnabled(enabled);
3231 }
3232 }
3233
Tony Mantler2558b502013-07-09 10:53:34 -07003234 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003235 // Public no-args constructor needed for fragment re-instantiation
3236 public DiscardConfirmDialogFragment() {}
3237
Tony Mantler2558b502013-07-09 10:53:34 -07003238 @Override
3239 public Dialog onCreateDialog(Bundle savedInstanceState) {
3240 return new AlertDialog.Builder(getActivity())
3241 .setMessage(R.string.confirm_discard_text)
3242 .setPositiveButton(R.string.discard,
3243 new DialogInterface.OnClickListener() {
3244 @Override
3245 public void onClick(DialogInterface dialog, int which) {
3246 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3247 }
3248 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003249 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003250 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003251 }
3252 }
3253
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003254 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003255 final DialogFragment frag = new DiscardConfirmDialogFragment();
3256 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003257 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003258 /**
3259 * Effectively discard the current message.
3260 *
3261 * This method is either invoked from the menu or from the dialog
3262 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003263 */
Tony Mantler2558b502013-07-09 10:53:34 -07003264 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003265 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003266 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3267 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003268 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003269 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003270 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3271 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003272 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003273 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003274 // This is not strictly necessary (since we should not try to
3275 // save the draft after calling this) but it ensures that if we
3276 // do save again for some reason we make a new draft rather than
3277 // trying to resave an expunged draft.
3278 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3279 }
3280 }
3281
Tony Mantler2558b502013-07-09 10:53:34 -07003282 // Display a toast to let the user know
3283 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003284
3285 // This prevents the draft from being saved in onPause().
3286 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003287 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003288 finish();
3289 }
3290
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003291 private void saveIfNeeded() {
3292 if (mAccount == null) {
3293 // We have not chosen an account yet so there's no way that we can save. This is ok,
3294 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3295 // user has not interacted with us yet and there is no real state to save.
3296 return;
3297 }
3298
3299 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003300 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003301 }
3302 }
3303
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003304 @Override
3305 public void onAttachmentDeleted() {
3306 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003307 // If we are showing any attachments, make sure we have an upper
3308 // divider.
3309 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003310 updateSaveUi();
3311 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003312
mindyp40882432012-09-06 11:07:40 -07003313 @Override
3314 public void onAttachmentAdded() {
3315 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3316 mAttachmentsView.focusLastAttachment();
3317 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003318
3319 /**
3320 * This is called any time one of our text fields changes.
3321 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003322 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003323 public void afterTextChanged(Editable s) {
3324 mTextChanged = true;
3325 updateSaveUi();
3326 }
3327
3328 @Override
3329 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3330 // Do nothing.
3331 }
3332
Marc Blank0bbc8582012-04-23 15:07:57 -07003333 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003334 public void onTextChanged(CharSequence s, int start, int before, int count) {
3335 // Do nothing.
3336 }
3337
3338
3339 // There is a big difference between the text associated with an address changing
3340 // to add the display name or to format properly and a recipient being added or deleted.
3341 // Make sure we only notify of changes when a recipient has been added or deleted.
3342 private class RecipientTextWatcher implements TextWatcher {
3343 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3344
3345 private RecipientEditTextView mView;
3346
3347 private TextWatcher mListener;
3348
3349 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3350 mView = view;
3351 mListener = listener;
3352 }
3353
3354 @Override
3355 public void afterTextChanged(Editable s) {
3356 if (hasChanged()) {
3357 mListener.afterTextChanged(s);
3358 }
3359 }
3360
3361 private boolean hasChanged() {
3362 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
3363 int totalCount = currRecips.length;
3364 int totalPrevCount = 0;
3365 for (Entry<String, Integer> entry : mContent.entrySet()) {
3366 totalPrevCount += entry.getValue();
3367 }
3368 if (totalCount != totalPrevCount) {
3369 return true;
3370 }
3371
3372 for (String recip : currRecips) {
3373 if (!mContent.containsKey(recip)) {
3374 return true;
3375 } else {
3376 int count = mContent.get(recip) - 1;
3377 if (count < 0) {
3378 return true;
3379 } else {
3380 mContent.put(recip, count);
3381 }
3382 }
3383 }
3384 return false;
3385 }
3386
3387 private String[] tokenizeRecips(String[] recips) {
3388 // Tokenize them all and put them in the list.
3389 String[] recipAddresses = new String[recips.length];
3390 for (int i = 0; i < recips.length; i++) {
3391 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
3392 }
3393 return recipAddresses;
3394 }
3395
3396 @Override
3397 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3398 String[] recips = tokenizeRecips(getAddressesFromList(mView));
3399 for (String recip : recips) {
3400 if (!mContent.containsKey(recip)) {
3401 mContent.put(recip, 1);
3402 } else {
3403 mContent.put(recip, (mContent.get(recip)) + 1);
3404 }
3405 }
3406 }
3407
3408 @Override
3409 public void onTextChanged(CharSequence s, int start, int before, int count) {
3410 // Do nothing.
3411 }
3412 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003413
3414 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3415 if (sTestSendOrSaveCallback != null && testCallback != null) {
3416 throw new IllegalStateException("Attempting to register more than one test callback");
3417 }
3418 sTestSendOrSaveCallback = testCallback;
3419 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003420
3421 @VisibleForTesting
3422 protected ArrayList<Attachment> getAttachments() {
3423 return mAttachmentsView.getAttachments();
3424 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003425
3426 @Override
3427 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3428 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003429 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3430 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3431 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003432 case REFERENCE_MESSAGE_LOADER:
3433 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3434 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003435 case LOADER_ACCOUNT_CURSOR:
3436 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3437 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003438 }
3439 return null;
3440 }
3441
3442 @Override
3443 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003444 int id = loader.getId();
3445 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003446 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003447 if (data != null && data.moveToFirst()) {
3448 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003449 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003450 initFromRefMessage(mComposeMode);
3451 finishSetup(mComposeMode, intent, null);
3452 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003453 String to = intent.getStringExtra(EXTRA_TO);
3454 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003455 mRefMessage.setTo(null);
3456 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003457 clearChangeListeners();
3458 mTo.append(to);
3459 initChangeListeners();
3460 }
3461 }
3462 } else {
3463 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003464 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003465 break;
Alice Yanga990a712013-03-13 18:37:00 -07003466 case REFERENCE_MESSAGE_LOADER:
3467 // Only populate mRefMessage and leave other fields untouched.
3468 if (data != null && data.moveToFirst()) {
3469 mRefMessage = new Message(data);
3470 }
Andy Huang9f855d62013-05-30 17:15:03 -07003471 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003472 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003473 case LOADER_ACCOUNT_CURSOR:
3474 if (data != null && data.moveToFirst()) {
3475 // there are accounts now!
3476 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003477 final ArrayList<Account> accounts = new ArrayList<Account>();
3478 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003479 do {
3480 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003481 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003482 initializedAccounts.add(account);
3483 }
3484 accounts.add(account);
3485 } while (data.moveToNext());
3486 if (initializedAccounts.size() > 0) {
3487 findViewById(R.id.wait).setVisibility(View.GONE);
3488 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3489 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003490 mAccounts = initializedAccounts.toArray(
3491 new Account[initializedAccounts.size()]);
3492
Mindy Pereirab199d172012-08-13 11:04:03 -07003493 finishCreate();
3494 invalidateOptionsMenu();
3495 } else {
3496 // Show "waiting"
3497 account = accounts.size() > 0 ? accounts.get(0) : null;
3498 showWaitFragment(account);
3499 }
3500 }
3501 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003502 }
3503 }
3504
Mindy Pereirab199d172012-08-13 11:04:03 -07003505 private void showWaitFragment(Account account) {
3506 WaitFragment fragment = getWaitFragment();
3507 if (fragment != null) {
3508 fragment.updateAccount(account);
3509 } else {
3510 findViewById(R.id.wait).setVisibility(View.VISIBLE);
3511 replaceFragment(WaitFragment.newInstance(account, true),
3512 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3513 }
3514 }
3515
3516 private WaitFragment getWaitFragment() {
3517 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3518 }
3519
3520 private int replaceFragment(Fragment fragment, int transition, String tag) {
3521 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003522 fragmentTransaction.setTransition(transition);
3523 fragmentTransaction.replace(R.id.wait, fragment, tag);
3524 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3525 return transactionId;
3526 }
3527
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003528 @Override
3529 public void onLoaderReset(Loader<Cursor> arg0) {
3530 // Do nothing.
3531 }
Paul Westbrook83e6b572013-02-05 16:22:42 -08003532
3533 @Override
3534 public Context getActivityContext() {
3535 return this;
3536 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003537}