blob: c1dd6b1210f1bc2053e9941861008ea0ce2d5be9 [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;
Andy Huang5c5fd572012-04-08 18:19:29 -070075import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080076import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070077import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070078import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070079import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070080import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080081import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080082import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080083import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080084import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080085import com.android.mail.providers.Address;
86import 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
162 private static final String EXTRA_TO = "to";
163 private static final String EXTRA_CC = "cc";
164 private static final String EXTRA_BCC = "bcc";
165
Scott Kennedy60847252013-08-15 15:55:42 -0700166 /**
167 * An optional extra containing a {@link ContentValues} of values to be added to
168 * {@link SendOrSaveMessage#mValues}.
169 */
170 public static final String EXTRA_VALUES = "extra-values";
171
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700172 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700173 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
174 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700175
Mindy Pereira82cc5662012-01-09 17:29:30 -0800176 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
177 // Map containing information about requests to create new messages, and the id of the
178 // messages that were the result of those requests.
179 //
180 // This map is used when the activity that initiated the save a of a new message, is killed
181 // before the save has completed (and when we know the id of the newly created message). When
182 // a save is completed, the service that is running in the background, will update the map
183 //
184 // When a new ComposeActivity instance is created, it will attempt to use the information in
185 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
186 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
187 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700188 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800189 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800190 /**
191 * Notifies the {@code Activity} that the caller is an Email
192 * {@code Activity}, so that the back behavior may be modified accordingly.
193 *
194 * @see #onAppUpPressed
195 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700196 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800197
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700198 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700199
Scott Kennedy5680ec22013-01-07 13:15:20 -0800200 /** If set, we will clear notifications for this folder. */
201 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
202
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800203 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700204 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700205 // If this is a reply/forward then this extra will hold a uri we must query
206 // to get the original message.
207 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700208 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700209 // draft message
210 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800211 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700212 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800213 // Request numbers for activities we start
214 private static final int RESULT_PICK_ATTACHMENT = 1;
215 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700216 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700217 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700218
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700219 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
220 private static final String EXTRA_REQUEST_ID = "requestId";
221 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700222 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700223 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700224 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700225 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700226 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700227 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700228 private static final String TAG_WAIT = "wait-fragment";
Andrew Sapperstein5cb71802013-10-01 18:31:20 -0700229 private static final String MIME_TYPE_ALL = "*/*";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700230 private static final String MIME_TYPE_PHOTO = "image/*";
231 private static final String MIME_TYPE_VIDEO = "video/*";
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;
266 private MenuItem mSend;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700267 @VisibleForTesting
268 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800269 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
270 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800271 private ReplyFromAccount mDraftAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800272 private Object mDraftLock = new Object();
mindyp93b079b2012-08-29 16:32:15 -0700273 private View mPhotoAttachmentsButton;
274 private View mVideoAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800275
Mindy Pereira326c6602012-01-04 15:32:42 -0800276 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700277 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
278 */
279 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700280 private RecipientTextWatcher mToListener;
281 private RecipientTextWatcher mCcListener;
282 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700283 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700284 private boolean mShowQuotedText = false;
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700285 protected Bundle mInnerSavedState;
Scott Kennedy60847252013-08-15 15:55:42 -0700286 private ContentValues mExtraValues = null;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700287
mindyp1623f9b2012-11-21 12:41:16 -0800288 // Array of the outstanding send or save tasks. Access is synchronized
289 // with the object itself
290 /* package for testing */
291 @VisibleForTesting
292 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
293 // FIXME: this variable is never read. related to sRequestMessageIdMap.
294 private int mRequestId;
295 private String mSignature;
296 private Account[] mAccounts;
297 private boolean mRespondedInline;
Andy Huangdc97bf42013-08-15 16:52:45 -0700298 private boolean mPerformedSendOrDiscard = false;
mindyp1623f9b2012-11-21 12:41:16 -0800299
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700300 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800301 * Can be called from a non-UI thread.
302 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800303 public static void editDraft(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700304 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
305 null /* extraValues */);
Mindy Pereira326c6602012-01-04 15:32:42 -0800306 }
307
Mindy Pereira6349a042012-01-04 11:25:01 -0800308 /**
309 * Can be called from a non-UI thread.
310 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800311 public static void compose(Context launcher, Account account) {
Scott Kennedy60847252013-08-15 15:55:42 -0700312 launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800313 }
314
315 /**
316 * Can be called from a non-UI thread.
317 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700318 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Scott Kennedy60847252013-08-15 15:55:42 -0700319 launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
320 null /* extraValues */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700321 }
322
323 /**
324 * Can be called from a non-UI thread.
325 */
326 public static void composeWithQuotedText(Context launcher, Account account,
Scott Kennedy60847252013-08-15 15:55:42 -0700327 String quotedText, String subject, final ContentValues extraValues) {
328 launch(launcher, account, null, COMPOSE, null, null, quotedText, subject, extraValues);
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700329 }
330
331 /**
332 * Can be called from a non-UI thread.
333 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700334 public static void composeWithExtraValues(Context launcher, Account account,
335 String subject, final ContentValues extraValues) {
336 launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
337 }
338
339 /**
340 * Can be called from a non-UI thread.
341 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800342 public static Intent createReplyIntent(final Context launcher, final Account account,
343 final Uri messageUri, final boolean isReplyAll) {
344 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
345 }
346
347 /**
348 * Can be called from a non-UI thread.
349 */
350 public static Intent createForwardIntent(final Context launcher, final Account account,
351 final Uri messageUri) {
352 return createActionIntent(launcher, account, messageUri, FORWARD);
353 }
354
355 private static Intent createActionIntent(final Context launcher, final Account account,
356 final Uri messageUri, final int action) {
357 final Intent intent = new Intent(launcher, ComposeActivity.class);
358
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700359 updateActionIntent(account, messageUri, action, intent);
360
361 return intent;
362 }
363
364 @VisibleForTesting
365 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800366 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
367 intent.putExtra(EXTRA_ACTION, action);
368 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
369 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
370
371 return intent;
372 }
373
374 /**
375 * Can be called from a non-UI thread.
376 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800377 public static void reply(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700378 launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800379 }
380
381 /**
382 * Can be called from a non-UI thread.
383 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800384 public static void replyAll(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700385 launch(launcher, account, message, REPLY_ALL, null, null, null, null,
386 null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800387 }
388
389 /**
390 * Can be called from a non-UI thread.
391 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800392 public static void forward(Context launcher, Account account, Message message) {
Scott Kennedy60847252013-08-15 15:55:42 -0700393 launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
Mindy Pereira6349a042012-01-04 11:25:01 -0800394 }
395
Alice Yang1ebc2db2013-03-14 21:21:44 -0700396 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
397 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700398 launch(launcher, account, message, FORWARD,
Scott Kennedy60847252013-08-15 15:55:42 -0700399 "android-gmail-readability@google.com", body, null, null, null /* extraValues */);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700400 }
401
402 private static void launch(Context launcher, Account account, Message message, int action,
Scott Kennedy60847252013-08-15 15:55:42 -0700403 String toAddress, String body, String quotedText, String subject,
404 final ContentValues extraValues) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800405 Intent intent = new Intent(launcher, ComposeActivity.class);
406 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
407 intent.putExtra(EXTRA_ACTION, action);
408 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700409 if (action == EDIT_DRAFT) {
410 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
411 } else {
412 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
413 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700414 if (toAddress != null) {
415 intent.putExtra(EXTRA_TO, toAddress);
416 }
417 if (body != null) {
418 intent.putExtra(EXTRA_BODY, body);
419 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700420 if (quotedText != null) {
421 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
422 }
423 if (subject != null) {
424 intent.putExtra(EXTRA_SUBJECT, subject);
425 }
Scott Kennedy60847252013-08-15 15:55:42 -0700426 if (extraValues != null) {
427 LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
428 intent.putExtra(EXTRA_VALUES, extraValues);
429 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800430 launcher.startActivity(intent);
431 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800432
433 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700434 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800435 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800436 setContentView(R.layout.compose);
Andy Huang9f855d62013-05-30 17:15:03 -0700437 mInnerSavedState = (savedInstanceState != null) ?
438 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700439 checkValidAccounts();
440 }
441
442 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700443 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800444 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800445 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700446 Message message;
Mark Wei62066e42012-09-13 12:07:02 -0700447 ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700448 mShowQuotedText = false;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700449 CharSequence quotedText = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700450 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700451 // Check for any of the possibly supplied accounts.;
452 Account account = null;
Andy Huang9f855d62013-05-30 17:15:03 -0700453 if (hadSavedInstanceStateMessage(savedState)) {
454 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
455 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
456 message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700457
Andy Huang9f855d62013-05-30 17:15:03 -0700458 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
459 mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700460 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Scott Kennedy44d44812013-08-19 14:18:31 -0700461
462 mExtraValues = savedState.getParcelable(EXTRA_VALUES);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700463 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700464 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700465 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
466 // Initialize the message from the message in the intent
467 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700468 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700469 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700470 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
Andy Huang4fe0af82013-08-20 17:24:51 -0700471
472 if (Analytics.isLoggable()) {
473 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
474 Analytics.getInstance().sendEvent(
475 "notification_action", "compose", getActionString(action), 0);
476 }
477 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700478 }
Mark Wei62066e42012-09-13 12:07:02 -0700479 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700480
481 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800482 if (mAccount == null) {
483 return;
484 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700485
Scott Kennedyfe853d32013-06-19 11:47:35 -0700486 initRecipients();
487
Scott Kennedy5680ec22013-01-07 13:15:20 -0800488 // Clear the notification and mark the conversation as seen, if necessary
489 final Folder notificationFolder =
490 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
491 if (notificationFolder != null) {
492 final Intent clearNotifIntent =
493 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800494 clearNotifIntent.setPackage(getPackageName());
Scott Kennedy48cfe462013-04-10 11:32:02 -0700495 clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
496 clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800497
498 startService(clearNotifIntent);
499 }
500
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700501 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
502 mLaunchedFromEmail = true;
503 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
504 final Uri dataUri = intent.getData();
505 if (dataUri != null) {
506 final String dataScheme = intent.getData().getScheme();
507 final String accountScheme = mAccount.composeIntentUri.getScheme();
508 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
509 }
510 }
511
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700512 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700513 mShowQuotedText = true;
514 mComposeMode = action;
515 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700516 return;
517 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700518 initFromDraftMessage(message);
519 initQuotedTextFromRefMessage(mRefMessage, action);
Andy Huang9f855d62013-05-30 17:15:03 -0700520 showCcBcc(savedState);
Alice Yanga990a712013-03-13 18:37:00 -0700521 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700522 // if we should be showing quoted text but mRefMessage is null
523 // and we have some quotedText, display that
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700524 if (mShowQuotedText && mRefMessage == null) {
525 if (quotedText != null) {
526 initQuotedText(quotedText, false /* shouldQuoteText */);
527 } else if (mExtraValues != null) {
528 initExtraValues(mExtraValues);
529 return;
530 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700531 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700532 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700533 initFromDraftMessage(message);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800534 boolean showBcc = !TextUtils.isEmpty(message.getBcc());
535 boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
Mindy Pereiraef388302012-06-18 19:07:44 -0700536 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700537 // Update the action to the draft type of the previous draft
538 switch (message.draftType) {
539 case UIProvider.DraftType.REPLY:
540 action = REPLY;
541 break;
542 case UIProvider.DraftType.REPLY_ALL:
543 action = REPLY_ALL;
544 break;
545 case UIProvider.DraftType.FORWARD:
546 action = FORWARD;
547 break;
548 case UIProvider.DraftType.COMPOSE:
549 default:
550 action = COMPOSE;
551 break;
552 }
Alice Yanga990a712013-03-13 18:37:00 -0700553 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
554
555 mShowQuotedText = message.appendRefMessageContent;
556 if (message.refMessageUri != null) {
557 // If we're editing an existing draft that was in reference to an existing message,
558 // still need to load that original message since we might need to refer to the
559 // original sender and recipients if user switches "reply <-> reply-all".
560 mRefMessageUri = message.refMessageUri;
561 mComposeMode = action;
562 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
563 return;
564 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700565 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
566 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800567 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700568 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700569 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700570 } else {
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700571 if (initFromExtras(intent)) {
572 return;
573 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700574 }
Alice Yanga990a712013-03-13 18:37:00 -0700575
576 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700577 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700578 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700579
Mindy Pereirab199d172012-08-13 11:04:03 -0700580 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700581 final Account[] allAccounts = AccountUtils.getAccounts(this);
582 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700583 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
584 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700585 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700586 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
587 }
588 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700589 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700590 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700591 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700592 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700593 anySyncing = true;
594 break;
595 }
596 }
597 if (!anySyncing) {
598 // There are accounts, but none are sync'd, which is just like having no accounts.
599 mAccounts = null;
600 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
601 return;
602 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700603 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700604 finishCreate();
605 }
606 }
607
Mindy Pereira47d0e652012-07-23 09:45:07 -0700608 private Account obtainAccount(Intent intent) {
609 Account account = null;
610 Object accountExtra = null;
611 if (intent != null && intent.getExtras() != null) {
612 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
613 if (accountExtra instanceof Account) {
614 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700615 } else if (accountExtra instanceof String) {
616 // This is the Account attached to the widget compose intent.
617 account = Account.newinstance((String)accountExtra);
618 if (account != null) {
619 return account;
620 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700621 }
mindyp5ee9dc42013-01-08 09:54:54 -0800622 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
623 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
624 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700625 }
626 if (account == null) {
mindyp06174462012-10-12 09:11:27 -0700627 MailAppProvider provider = MailAppProvider.getInstance();
628 String lastAccountUri = provider.getLastSentFromAccount();
629 if (TextUtils.isEmpty(lastAccountUri)) {
630 lastAccountUri = provider.getLastViewedAccount();
631 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700632 if (!TextUtils.isEmpty(lastAccountUri)) {
633 accountExtra = Uri.parse(lastAccountUri);
634 }
635 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700636 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700637 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
638 // For backwards compatibility, we need to check account
639 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700640 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700641 if (a.name.equals(accountExtra)) {
642 account = a;
643 }
644 }
645 } else if (accountExtra instanceof Uri) {
646 // The uri of the last viewed account is what is stored in
647 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700648 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700649 if (a.uri.equals(accountExtra)) {
650 account = a;
651 }
652 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700653 }
654 if (account == null) {
655 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700656 }
657 }
658 return account;
659 }
660
Andrew Sapperstein746d8612013-08-26 15:56:32 -0700661 protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800662 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700663 // Don't bother with the intent if we have procured a message from the
664 // intent already.
665 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
666 initAttachmentsFromIntent(intent);
667 }
Alice Yanga990a712013-03-13 18:37:00 -0700668 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700669 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
670 action);
mindypd4a48662012-11-08 17:13:49 -0800671
672 // If this is a draft message, the draft account is whatever account was
673 // used to open the draft message in Compose.
674 if (mDraft != null) {
675 mDraftAccount = mReplyFromAccount;
676 }
677
Mindy Pereira75f66632012-01-11 11:42:02 -0800678 initChangeListeners();
Mindy Pereira326689d2012-05-17 10:14:14 -0700679 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700680 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800681
Andy Huang9f855d62013-05-30 17:15:03 -0700682 mRespondedInline = mInnerSavedState != null ?
683 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
mindyp1623f9b2012-11-21 12:41:16 -0800684 if (mRespondedInline) {
685 mQuotedTextView.setVisibility(View.GONE);
686 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700687 }
688
Scott Kennedyff8553f2013-04-05 20:57:44 -0700689 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700690 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
691 }
692
Mindy Pereira71c9e562012-05-17 11:01:02 -0700693 private void updateHideOrShowQuotedText(boolean showQuotedText) {
694 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700695 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700696 }
697
698 private void setFocus(int action) {
699 if (action == EDIT_DRAFT) {
700 int type = mDraft.draftType;
701 switch (type) {
702 case UIProvider.DraftType.COMPOSE:
703 case UIProvider.DraftType.FORWARD:
704 action = COMPOSE;
705 break;
706 case UIProvider.DraftType.REPLY:
707 case UIProvider.DraftType.REPLY_ALL:
708 default:
709 action = REPLY;
710 break;
711 }
712 }
713 switch (action) {
714 case FORWARD:
715 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800716 if (TextUtils.isEmpty(mTo.getText())) {
717 mTo.requestFocus();
718 break;
719 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700720 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700721 case REPLY:
722 case REPLY_ALL:
723 default:
724 focusBody();
725 break;
726 }
727 }
728
729 /**
730 * Focus the body of the message.
731 */
732 public void focusBody() {
733 mBodyView.requestFocus();
734 int length = mBodyView.getText().length();
735
736 int signatureStartPos = getSignatureStartPosition(
737 mSignature, mBodyView.getText().toString());
738 if (signatureStartPos > -1) {
739 // In case the user deleted the newlines...
740 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700741 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700742 // Move cursor to the end.
743 mBodyView.setSelection(length);
744 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800745 }
746
747 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700748 protected void onStart() {
749 super.onStart();
750
751 Analytics.getInstance().activityStart(this);
752 }
753
754 @Override
755 protected void onStop() {
756 super.onStop();
757
758 Analytics.getInstance().activityStop(this);
759 }
760
761 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800762 protected void onResume() {
763 super.onResume();
764 // Update the from spinner as other accounts
765 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800766 if (mFromSpinner != null && mAccount != null) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700767 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800768 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800769 }
770
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800771 @Override
772 protected void onPause() {
773 super.onPause();
774
Mindy Pereiraa2148332012-07-02 13:54:14 -0700775 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700776 // Don't save unnecessary drafts if we are only changing the orientation.
777 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700778 saveIfNeeded();
Andy Huangdc97bf42013-08-15 16:52:45 -0700779
Andy Huange003b4c2013-08-16 10:32:05 -0700780 if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
Andy Huangdc97bf42013-08-15 16:52:45 -0700781 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
782 // because that method can be invoked many times in a single compose session.)
783 logSendOrSave(true /* save */);
784 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700785 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800786 }
787
788 @Override
789 protected final void onActivityResult(int request, int result, Intent data) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700790 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800791 addAttachmentAndUpdateView(data);
Mindy Pereirab199d172012-08-13 11:04:03 -0700792 mAddingAttachment = false;
793 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700794 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700795 if (result != RESULT_OK) {
796 finish();
797 } else {
798 // Watch for accounts to show up!
799 // restart the loader to get the updated list of accounts
800 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
801 showWaitFragment(null);
802 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800803 }
804 }
805
806 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700807 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700808 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
809 if (hasAccounts) {
810 clearChangeListeners();
811 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700812 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700813 if (mInnerSavedState != null) {
814 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
815 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
816 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700817 // There should be a focus and it should be an EditText since we
818 // only save these extras if these conditions are true.
819 EditText focusEditText = (EditText) getCurrentFocus();
820 final int length = focusEditText.getText().length();
821 if (selectionStart < length && selectionEnd < length) {
822 focusEditText.setSelection(selectionStart, selectionEnd);
823 }
824 }
825 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700826 if (hasAccounts) {
827 initChangeListeners();
828 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700829 }
830
831 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700832 protected final void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800833 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700834 final Bundle inner = new Bundle();
835 saveState(inner);
836 state.putBundle(KEY_INNER_SAVED_STATE, inner);
837 }
838
839 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700840 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
841 if (mAccounts == null || mAccounts.length == 0) {
842 return;
843 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700844 // The framework is happy to save and restore the selection but only if it also saves and
845 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
846 // this manually.
847 View focus = getCurrentFocus();
848 if (focus != null && focus instanceof EditText) {
849 EditText focusEditText = (EditText) focus;
850 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
851 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
852 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700853
854 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700855 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700856 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
857 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
858 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700859 if (selectedReplyFromAccount != null) {
860 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
861 .toString());
862 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
863 } else {
864 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
865 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800866
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700867 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
868 // We don't have a draft id, and we have a request id,
869 // save the request id.
870 state.putInt(EXTRA_REQUEST_ID, mRequestId);
871 }
872
873 // We want to restore the current mode after a pause
874 // or rotation.
875 int mode = getMode();
876 state.putInt(EXTRA_ACTION, mode);
877
mindype7b76aa2012-11-14 16:19:13 -0800878 final Message message = createMessage(selectedReplyFromAccount, mode);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700879 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -0800880 message.id = mDraft.id;
881 message.serverId = mDraft.serverId;
882 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700883 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700884 state.putParcelable(EXTRA_MESSAGE, message);
885
886 if (mRefMessage != null) {
887 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700888 } else if (message.appendRefMessageContent) {
889 // If we have no ref message but should be appending
890 // ref message content, we have orphaned quoted text. Save it.
891 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700892 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700893 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
894 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -0800895 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -0800896 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -0700897 state.putParcelableArrayList(
898 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Scott Kennedy44d44812013-08-19 14:18:31 -0700899
900 state.putParcelable(EXTRA_VALUES, mExtraValues);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700901 }
902
903 private int getMode() {
904 int mode = ComposeActivity.COMPOSE;
905 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700906 if (actionBar != null
907 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700908 mode = actionBar.getSelectedNavigationIndex();
909 }
910 return mode;
911 }
912
913 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
914 Message message = new Message();
915 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -0700916 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700917 message.uri = null;
918 message.conversationUri = null;
919 message.subject = mSubject.getText().toString();
920 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800921 message.setTo(formatSenders(mTo.getText().toString()));
922 message.setCc(formatSenders(mCc.getText().toString()));
923 message.setBcc(formatSenders(mBcc.getText().toString()));
924 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700925 message.dateReceivedMs = 0;
Paul Westbrookb4931c62013-01-14 17:51:18 -0800926 final String htmlBody = Html.toHtml(removeComposingSpans(mBodyView.getText()));
927 final StringBuilder fullBody = new StringBuilder(htmlBody);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700928 message.bodyHtml = fullBody.toString();
929 message.bodyText = mBodyView.getText().toString();
930 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -0700931 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700932 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
933 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
934 message.hasAttachments = attachments != null && attachments.size() > 0;
935 message.attachmentListUri = null;
936 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700937 message.alwaysShowImages = false;
938 message.attachmentsJson = Attachment.toJSONArray(attachments);
939 CharSequence quotedText = mQuotedTextView.getQuotedText();
940 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
941 .getQuotedTextOffset(quotedText.toString()) : -1;
942 message.accountUri = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800943 message.setFrom(selectedReplyFromAccount != null ? selectedReplyFromAccount.address
944 : mAccount != null ? mAccount.name : null);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700945 message.draftType = getDraftType(mode);
mindype7b76aa2012-11-14 16:19:13 -0800946 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700947 }
948
Scott Kennedyff8553f2013-04-05 20:57:44 -0700949 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -0700950 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
951 return string.substring(0, string.length() - 1);
952 }
953 return string;
954 }
955
Mindy Pereira818143e2012-01-11 13:59:49 -0800956 @VisibleForTesting
957 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700958 if (account == null) {
959 return;
960 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700961 if (!account.equals(mAccount)) {
962 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700963 mCachedSettings = mAccount.settings;
964 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700965 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700966 if (mAccount != null) {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800967 MailActivity.setNfcMessage(mAccount.name);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700968 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800969 }
970
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700971 private void initFromSpinner(Bundle bundle, int action) {
972 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700973 action = COMPOSE;
974 }
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700975 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
976
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700977 if (bundle != null) {
978 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
979 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
980 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
981 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700982 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700983 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
984 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700985 }
986 if (mReplyFromAccount == null) {
987 if (mDraft != null) {
988 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
989 } else if (mRefMessage != null) {
990 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
991 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700992 }
993 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -0700994 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700995 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700996
Mindy Pereira62de1b12012-04-06 12:17:56 -0700997 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700998
Mindy Pereira62de1b12012-04-06 12:17:56 -0700999 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001000 // If there is only 1 account, just show that account.
1001 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -07001002 // send mail from / save drafts to.
1003 mFromStatic.setVisibility(View.GONE);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001004 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001005 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001006 } else {
1007 mFromStatic.setVisibility(View.VISIBLE);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -07001008 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001009 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -07001010 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001011 }
1012
Mindy Pereira62de1b12012-04-06 12:17:56 -07001013 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
1014 if (refMessage.accountUri != null) {
1015 // This must be from combined inbox.
1016 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1017 for (ReplyFromAccount from : replyFromAccounts) {
1018 if (from.account.uri.equals(refMessage.accountUri)) {
1019 return from;
1020 }
1021 }
1022 return null;
1023 } else {
1024 return getReplyFromAccount(account, refMessage);
1025 }
1026 }
1027
1028 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -07001029 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -07001030 * return who the message should be sent from.
1031 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -07001032 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -07001033 * @return the address from which to reply.
1034 */
1035 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
1036 // First see if we are supposed to use the default address or
1037 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -07001038 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001039 return getDefaultReplyFromAccount(account);
1040 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -07001041 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -07001042 // all the message recipients and find one that matches
1043 // a custom from or account.
1044 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -07001045 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
1046 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -07001047 return getMatchingRecipient(account, allRecipients);
1048 }
1049 }
1050
1051 /**
1052 * Compare all the recipients of an email to the current account and all
1053 * custom addresses associated with that account. Return the match if there
1054 * is one, or the default account if there isn't.
1055 */
1056 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1057 // Tokenize the list and place in a hashmap.
1058 ReplyFromAccount matchingReplyFrom = null;
1059 Rfc822Token[] tokens;
1060 HashSet<String> recipientsMap = new HashSet<String>();
1061 for (String address : sentTo) {
1062 tokens = Rfc822Tokenizer.tokenize(address);
1063 for (int i = 0; i < tokens.length; i++) {
1064 recipientsMap.add(tokens[i].getAddress());
1065 }
1066 }
1067
1068 int matchingAddressCount = 0;
1069 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001070 customFroms = account.getReplyFroms();
1071 if (customFroms != null) {
1072 for (ReplyFromAccount entry : customFroms) {
1073 if (recipientsMap.contains(entry.address)) {
1074 matchingReplyFrom = entry;
1075 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001076 }
1077 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001078 }
1079 if (matchingAddressCount > 1) {
1080 matchingReplyFrom = getDefaultReplyFromAccount(account);
1081 }
1082 return matchingReplyFrom;
1083 }
1084
Scott Kennedyff8553f2013-04-05 20:57:44 -07001085 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1086 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001087 if (from.isDefault) {
1088 return from;
1089 }
1090 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -07001091 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
1092 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001093 }
1094
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001095 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001096 String sender = msg.getFrom();
Mindy Pereira62de1b12012-04-06 12:17:56 -07001097 ReplyFromAccount replyFromAccount = null;
1098 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1099 if (TextUtils.equals(account.name, sender)) {
1100 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -07001101 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001102 } else {
1103 for (ReplyFromAccount fromAccount : replyFromAccounts) {
1104 if (TextUtils.equals(fromAccount.name, sender)) {
1105 replyFromAccount = fromAccount;
1106 break;
1107 }
1108 }
1109 }
1110 return replyFromAccount;
1111 }
1112
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001113 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001114 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001115 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001116 if (mCcBccButton != null) {
1117 mCcBccButton.setOnClickListener(this);
1118 }
1119 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001120 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
mindyp93b079b2012-08-29 16:32:15 -07001121 mPhotoAttachmentsButton = findViewById(R.id.add_photo_attachment);
mindypcd0b0b92012-08-23 14:33:17 -07001122 if (mPhotoAttachmentsButton != null) {
1123 mPhotoAttachmentsButton.setOnClickListener(this);
1124 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001125 mTo = (RecipientEditTextView) findViewById(R.id.to);
Scott Kennedy41500392013-04-24 18:46:36 -07001126 mTo.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001127 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Scott Kennedy41500392013-04-24 18:46:36 -07001128 mCc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001129 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Scott Kennedy41500392013-04-24 18:46:36 -07001130 mBcc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001131 // TODO: add special chips text change watchers before adding
1132 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001133 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001134 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001135 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1136 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001137 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001138 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001139 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001140 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001141 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001142 }
1143
mindyp62d3ec72012-08-24 13:04:09 -07001144 @Override
1145 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1146 if (action == EditorInfo.IME_ACTION_DONE) {
1147 focusBody();
1148 return true;
1149 }
1150 return false;
1151 }
1152
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001153 protected TextView getBody() {
1154 return mBodyView;
1155 }
1156
1157 @VisibleForTesting
1158 public Account getFromAccount() {
1159 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1160 mReplyFromAccount.account : mAccount;
1161 }
1162
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001163 private void clearChangeListeners() {
1164 mSubject.removeTextChangedListener(this);
1165 mBodyView.removeTextChangedListener(this);
1166 mTo.removeTextChangedListener(mToListener);
1167 mCc.removeTextChangedListener(mCcListener);
1168 mBcc.removeTextChangedListener(mBccListener);
1169 mFromSpinner.setOnAccountChangedListener(null);
1170 mAttachmentsView.setAttachmentChangesListener(null);
1171 }
1172
Mindy Pereira75f66632012-01-11 11:42:02 -08001173 // Now that the message has been initialized from any existing draft or
1174 // ref message data, set up listeners for any changes that occur to the
1175 // message.
1176 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001177 // Make sure we only add text changed listeners once!
1178 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001179 mSubject.addTextChangedListener(this);
1180 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001181 if (mToListener == null) {
1182 mToListener = new RecipientTextWatcher(mTo, this);
1183 }
1184 mTo.addTextChangedListener(mToListener);
1185 if (mCcListener == null) {
1186 mCcListener = new RecipientTextWatcher(mCc, this);
1187 }
1188 mCc.addTextChangedListener(mCcListener);
1189 if (mBccListener == null) {
1190 mBccListener = new RecipientTextWatcher(mBcc, this);
1191 }
1192 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001193 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001194 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001195 }
1196
Alice Yanga990a712013-03-13 18:37:00 -07001197 private void initActionBar() {
1198 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001199 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001200 if (actionBar == null) {
1201 return;
1202 }
Alice Yanga990a712013-03-13 18:37:00 -07001203 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001204 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1205 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001206 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001207 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001208 if (mComposeModeAdapter == null) {
1209 mComposeModeAdapter = new ComposeModeAdapter(this);
1210 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001211 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1212 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001213 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001214 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001215 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001216 break;
1217 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001218 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001219 break;
1220 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001221 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001222 break;
1223 }
1224 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001225 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1226 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1227 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001228 }
1229
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001230 private void initFromRefMessage(int action) {
1231 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001232
1233 // Check if To: address and email body needs to be prefilled based on extras.
1234 // This is used for reporting rendering feedback.
1235 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1236 Intent intent = getIntent();
1237 if (intent.getExtras() != null) {
1238 String toAddresses = intent.getStringExtra(EXTRA_TO);
1239 if (toAddresses != null) {
1240 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1241 }
1242 String body = intent.getStringExtra(EXTRA_BODY);
1243 if (body != null) {
1244 setBody(body, false /* withSignature */);
1245 }
1246 }
1247 }
1248
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001249 if (mRefMessage != null) {
1250 // CC field only gets populated when doing REPLY_ALL.
1251 // BCC never gets auto-populated, unless the user is editing
1252 // a draft with one.
Mindy Pereira29a717e2012-07-25 18:05:48 -07001253 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001254 mCcBccView.show(false, true, false);
1255 }
1256 }
1257 updateHideOrShowCcBcc();
1258 }
1259
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001260 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001261 setSubject(mRefMessage, action);
1262 // Setup recipients
1263 if (action == FORWARD) {
1264 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001265 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001266 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001267 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001268 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1269 initAttachments(mRefMessage);
1270 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001271 }
1272
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001273 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001274 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001275
1276 mDraft = message;
1277 mDraftId = message.id;
1278 mSubject.setText(message.subject);
1279 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001280 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001281 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001282 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1283 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001284 if (message.hasAttachments) {
1285 List<Attachment> attachments = message.getAttachments();
1286 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001287 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001288 }
1289 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -07001290 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -07001291 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001292 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001293 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001294 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001295 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001296 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001297 // Find the offset in the htmltext of the actual quoted text and strip it out.
1298 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1299 if (quotedTextIndex > -1) {
mindypc59dd822012-11-13 10:56:21 -08001300 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1301 quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001302 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1303 message.bodyHtml.length());
1304 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001305 } else {
mindypc59dd822012-11-13 10:56:21 -08001306 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001307 }
1308 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001309 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001310 final String body = message.bodyText;
1311 final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1312 (quotedTextIndex > -1 ?
1313 message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1314 : "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001315 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001316 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1317 : null;
Mindy Pereira002ff522012-05-30 10:31:26 -07001318 }
1319 mBodyView.setText(bodyText);
1320 }
1321 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001322 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001323 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001324 }
1325
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001326 /**
1327 * Fill all the widgets with the content found in the Intent Extra, if any.
1328 * Also apply the same style to all widgets. Note: if initFromExtras is
1329 * called as a result of switching between reply, reply all, and forward per
1330 * the latest revision of Gmail, and the user has already made changes to
1331 * attachments on a previous incarnation of the message (as a reply, reply
1332 * all, or forward), the original attachments from the message will not be
1333 * re-instantiated. The user's changes will be respected. This follows the
1334 * web gmail interaction.
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001335 * @return {@code true} if the activity should not call {@link #finishSetup}.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001336 */
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001337 public boolean initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001338 // If we were invoked with a SENDTO intent, the value
1339 // should take precedence
1340 final Uri dataUri = intent.getData();
1341 if (dataUri != null) {
1342 if (MAIL_TO.equals(dataUri.getScheme())) {
1343 initFromMailTo(dataUri.toString());
1344 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001345 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001346 String toText = dataUri.getSchemeSpecificPart();
1347 if (toText != null) {
1348 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001349 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001350 }
1351 }
1352 }
1353 }
1354
1355 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1356 if (extraStrings != null) {
1357 addToAddresses(Arrays.asList(extraStrings));
1358 }
1359 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1360 if (extraStrings != null) {
1361 addCcAddresses(Arrays.asList(extraStrings), null);
1362 }
1363 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1364 if (extraStrings != null) {
1365 addBccAddresses(Arrays.asList(extraStrings));
1366 }
1367
1368 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1369 if (extraString != null) {
1370 mSubject.setText(extraString);
1371 }
1372
1373 for (String extra : ALL_EXTRAS) {
1374 if (intent.hasExtra(extra)) {
1375 String value = intent.getStringExtra(extra);
1376 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001377 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001378 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001379 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001380 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001381 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001382 } else if (EXTRA_SUBJECT.equals(extra)) {
1383 mSubject.setText(value);
1384 } else if (EXTRA_BODY.equals(extra)) {
1385 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001386 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1387 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001388 }
1389 }
1390 }
1391
1392 Bundle extras = intent.getExtras();
1393 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001394 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1395 if (text != null) {
1396 setBody(text, true /* with signature */);
1397 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001398
1399 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001400 }
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001401
1402 mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1403 if (mExtraValues != null) {
1404 LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1405 initExtraValues(mExtraValues);
1406 return true;
1407 }
1408
1409 return false;
1410 }
1411
1412 protected void initExtraValues(ContentValues extraValues) {
1413 // DO NOTHING - Gmail will override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001414 }
1415
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001416
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001417 @VisibleForTesting
1418 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001419 // TODO: handle the case where there are spaces in the display name as
1420 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1421 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001422 // Since URLDecode.decode changes + into ' ', and + is a valid
1423 // email character, we need to find/ replace these ourselves before
1424 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001425 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001426 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001427 } catch (IllegalArgumentException e) {
1428 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1429 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1430 } else {
1431 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1432 }
1433 return null;
1434 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001435 }
1436
1437 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001438 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1439 * changing '+' into ' '
1440 *
1441 * @param toReplace Input string
1442 * @return The string with all "+" characters replaced with "%2B"
1443 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001444 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001445 return toReplace.replace("+", "%2B");
1446 }
1447
1448 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001449 * Initialize the compose view from a String representing a mailTo uri.
1450 * @param mailToString The uri as a string.
1451 */
1452 public void initFromMailTo(String mailToString) {
1453 // We need to disguise this string as a URI in order to parse it
1454 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1455 Uri uri = Uri.parse("foo://" + mailToString);
1456 int index = mailToString.indexOf("?");
1457 int length = "mailto".length() + 1;
1458 String to;
1459 try {
1460 // Extract the recipient after mailto:
1461 if (index == -1) {
1462 to = decodeEmailInUri(mailToString.substring(length));
1463 } else {
1464 to = decodeEmailInUri(mailToString.substring(length, index));
1465 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001466 if (!TextUtils.isEmpty(to)) {
1467 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1468 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001469 } catch (UnsupportedEncodingException e) {
1470 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1471 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1472 } else {
1473 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1474 }
1475 }
1476
1477 List<String> cc = uri.getQueryParameters("cc");
1478 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1479
1480 List<String> otherTo = uri.getQueryParameters("to");
1481 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1482
1483 List<String> bcc = uri.getQueryParameters("bcc");
1484 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1485
1486 List<String> subject = uri.getQueryParameters("subject");
1487 if (subject.size() > 0) {
1488 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001489 mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
1490 UTF8_ENCODING_NAME));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001491 } catch (UnsupportedEncodingException e) {
1492 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1493 e.getMessage(), subject);
1494 }
1495 }
1496
1497 List<String> body = uri.getQueryParameters("body");
1498 if (body.size() > 0) {
1499 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001500 setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001501 true /* with signature */);
1502 } catch (UnsupportedEncodingException e) {
1503 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1504 }
1505 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001506 }
1507
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001508 @VisibleForTesting
1509 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001510 addAttachments(refMessage.getAttachments());
1511 }
1512
1513 public long addAttachments(List<Attachment> attachments) {
1514 long size = 0;
1515 AttachmentFailureException error = null;
1516 for (Attachment a : attachments) {
1517 try {
1518 size += mAttachmentsView.addAttachment(mAccount, a);
1519 } catch (AttachmentFailureException e) {
1520 error = e;
1521 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001522 }
Mark Wei434f2942012-08-24 11:54:02 -07001523 if (error != null) {
1524 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1525 if (attachments.size() > 1) {
1526 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1527 } else {
1528 showAttachmentTooBigToast(error.getErrorRes());
1529 }
1530 }
1531 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001532 }
1533
1534 /**
1535 * When an attachment is too large to be added to a message, show a toast.
1536 * This method also updates the position of the toast so that it is shown
1537 * clearly above they keyboard if it happens to be open.
1538 */
Mark Wei434f2942012-08-24 11:54:02 -07001539 private void showAttachmentTooBigToast(int errorRes) {
1540 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1541 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1542 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001543 }
1544
Mark Wei434f2942012-08-24 11:54:02 -07001545 private void showErrorToast(String message) {
1546 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1547 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001548 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1549 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1550 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001551 }
1552
Paul Westbrookf97588b2012-03-20 11:11:37 -07001553 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001554 Bundle extras = intent.getExtras();
1555 if (extras == null) {
1556 extras = Bundle.EMPTY;
1557 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001558 final String action = intent.getAction();
1559 if (!mAttachmentsChanged) {
1560 long totalSize = 0;
1561 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1562 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1563 for (String uriString : uris) {
1564 final Uri uri = Uri.parse(uriString);
1565 long size = 0;
1566 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001567 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1568 size = mAttachmentsView.addAttachment(mAccount, a);
1569
1570 Analytics.getInstance().sendEvent("send_intent_attachment",
1571 Utils.normalizeMimeType(a.getContentType()), null, size);
1572
Paul Westbrookf97588b2012-03-20 11:11:37 -07001573 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001574 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001575 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001576 }
1577 totalSize += size;
1578 }
1579 }
mindyp9a9e8d62012-10-03 12:24:07 -07001580 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1581 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1582 ArrayList<Parcelable> uris = extras
1583 .getParcelableArrayList(Intent.EXTRA_STREAM);
1584 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1585 for (Parcelable uri : uris) {
1586 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001587 final Attachment a = mAttachmentsView.generateLocalAttachment(
1588 (Uri) uri);
1589 attachments.add(a);
1590
1591 Analytics.getInstance().sendEvent("send_intent_attachment",
1592 Utils.normalizeMimeType(a.getContentType()), null, a.size);
1593
mindyp9a9e8d62012-10-03 12:24:07 -07001594 } catch (AttachmentFailureException e) {
1595 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1596 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1597 getApplicationContext(),
1598 mAccount.settings.getMaxAttachmentSize());
1599 showErrorToast(getString
1600 (R.string.generic_attachment_problem, maxSize));
1601 }
1602 }
1603 totalSize += addAttachments(attachments);
1604 } else {
1605 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1606 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001607 try {
Andy Huange003b4c2013-08-16 10:32:05 -07001608 final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1609 size = mAttachmentsView.addAttachment(mAccount, a);
1610
1611 Analytics.getInstance().sendEvent("send_intent_attachment",
1612 Utils.normalizeMimeType(a.getContentType()), null, size);
1613
Paul Westbrookf97588b2012-03-20 11:11:37 -07001614 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001615 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001616 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001617 }
mindyp9a9e8d62012-10-03 12:24:07 -07001618 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001619 }
1620 }
1621
1622 if (totalSize > 0) {
1623 mAttachmentsChanged = true;
1624 updateSaveUi();
Andy Huange003b4c2013-08-16 10:32:05 -07001625
1626 Analytics.getInstance().sendEvent("send_intent_with_attachments",
1627 Integer.toString(getAttachments().size()), null, totalSize);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001628 }
1629 }
1630 }
1631
Andrew Sapperstein746d8612013-08-26 15:56:32 -07001632 protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001633 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1634 mShowQuotedText = true;
1635 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001636
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001637 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1638 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001639 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1640 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001641 }
1642
1643 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001644 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001645 boolean ccVisible = mCcBccView.isCcVisible();
1646 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001647 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001648 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001649 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001650 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001651 : R.string.add_bcc_label));
1652 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001653 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001654 }
1655 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001656 }
1657
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001658 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001659 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1660 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1661 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1662 if (showCc || showBcc) {
1663 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001664 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001665 }
1666 }
1667
Mindy Pereira013194c2012-01-06 15:09:33 -08001668 /**
1669 * Add attachment and update the compose area appropriately.
1670 * @param data
1671 */
1672 public void addAttachmentAndUpdateView(Intent data) {
Andrew Sapperstein05089f32013-10-01 17:00:03 -07001673 if (data == null) {
1674 return;
1675 }
1676
1677 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1678 final ClipData clipData = data.getClipData();
1679 if (clipData != null) {
1680 for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1681 addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1682 }
1683 return;
1684 }
1685 }
1686
1687 addAttachmentAndUpdateView(data.getData());
Mindy Pereira2421dc82012-03-27 13:32:31 -07001688 }
1689
Andy Huang5c5fd572012-04-08 18:19:29 -07001690 public void addAttachmentAndUpdateView(Uri contentUri) {
1691 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001692 return;
1693 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001694 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001695 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1696 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001697 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001698 showErrorToast(getResources().getString(
1699 e.getErrorRes(),
1700 AttachmentUtils.convertToHumanReadableSize(
1701 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001702 }
1703 }
1704
1705 public void addAttachmentAndUpdateView(Attachment attachment) {
1706 try {
Mark Wei434f2942012-08-24 11:54:02 -07001707 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001708 if (size > 0) {
1709 mAttachmentsChanged = true;
1710 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001711 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001712 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001713 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001714 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001715 }
1716 }
1717
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001718 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001719 // Don't populate the address if this is a forward.
1720 if (action == ComposeActivity.FORWARD) {
1721 return;
1722 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001723 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001724 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001725
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001726 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1727 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001728 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001729 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001730 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001731 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001732 final String[] replyToAddresses = refMessage.getReplyToAddressesUnescaped();
1733 String replyToAddress = replyToAddresses.length > 0 ? replyToAddresses[0] : null;
1734 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1735 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
1736
mindyp65b06f52012-11-21 10:35:08 -08001737 // If there is no reply to address, the reply to address is the sender.
Tony Mantler89de9eb2013-07-25 11:43:58 -07001738 if (TextUtils.isEmpty(replyToAddress)) {
1739 replyToAddress = fromAddress;
mindyp65b06f52012-11-21 10:35:08 -08001740 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001741
1742 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1743 // Cc list is the union of the To and Cc recipients of the original
1744 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001745 // already on the To list.
1746 if (action == ComposeActivity.REPLY) {
Tony Mantler89de9eb2013-07-25 11:43:58 -07001747 toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001748 addToAddresses(toAddresses);
1749 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001750 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler89de9eb2013-07-25 11:43:58 -07001751 toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001752 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001753 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001754 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001755 addCcAddresses(ccAddresses, toAddresses);
1756 }
1757 }
1758
1759 private void addToAddresses(Collection<String> addresses) {
1760 addAddressesToList(addresses, mTo);
1761 }
1762
1763 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001764 addCcAddressesToList(tokenizeAddressList(addresses),
1765 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001766 }
1767
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001768 private void addBccAddresses(Collection<String> addresses) {
1769 addAddressesToList(addresses, mBcc);
1770 }
1771
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001772 @VisibleForTesting
1773 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1774 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1775 String address;
1776
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001777 if (compareToList == null) {
1778 for (Rfc822Token[] tokens : addresses) {
1779 for (int i = 0; i < tokens.length; i++) {
1780 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001781 list.append(address + END_TOKEN);
1782 }
1783 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001784 } else {
1785 HashSet<String> compareTo = convertToHashSet(compareToList);
1786 for (Rfc822Token[] tokens : addresses) {
1787 for (int i = 0; i < tokens.length; i++) {
1788 address = tokens[i].toString();
1789 // Check if this is a duplicate:
1790 if (!compareTo.contains(tokens[i].getAddress())) {
1791 // Get the address here
1792 list.append(address + END_TOKEN);
1793 }
1794 }
1795 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001796 }
1797 }
1798
Scott Kennedyff8553f2013-04-05 20:57:44 -07001799 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
1800 final HashSet<String> hash = new HashSet<String>();
1801 for (final Rfc822Token[] tokens : list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001802 for (int i = 0; i < tokens.length; i++) {
1803 hash.add(tokens[i].getAddress());
1804 }
1805 }
1806 return hash;
1807 }
1808
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001809 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1810 @VisibleForTesting
1811 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1812
1813 for (String address: addresses) {
1814 tokenized.add(Rfc822Tokenizer.tokenize(address));
1815 }
1816 return tokenized;
1817 }
1818
1819 @VisibleForTesting
1820 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1821 for (String address : addresses) {
1822 addAddressToList(address, list);
1823 }
1824 }
1825
Scott Kennedyff8553f2013-04-05 20:57:44 -07001826 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001827 if (address == null || list == null)
1828 return;
1829
Scott Kennedyff8553f2013-04-05 20:57:44 -07001830 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001831
1832 for (int i = 0; i < tokens.length; i++) {
1833 list.append(tokens[i] + END_TOKEN);
1834 }
1835 }
1836
1837 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001838 protected Collection<String> initToRecipients(final String fullSenderAddress,
1839 final String replyToAddress, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001840 // The To recipient is the reply-to address specified in the original
1841 // message, unless it is:
1842 // the current user OR a custom from of the current user, in which case
1843 // it's the To recipient list of the original message.
1844 // OR missing, in which case use the sender of the original message
1845 Set<String> toAddresses = Sets.newHashSet();
mindyp65b06f52012-11-21 10:35:08 -08001846 if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001847 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001848 } else {
mindyp65b06f52012-11-21 10:35:08 -08001849 // In this case, the user is replying to a message in which their
1850 // current account or one of their custom from addresses is the only
1851 // recipient and they sent the original message.
1852 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
1853 && recipientMatchesThisAccount(inToAddresses[0])) {
1854 toAddresses.add(inToAddresses[0]);
1855 return toAddresses;
1856 }
1857 // This happens if the user replies to a message they originally
1858 // wrote. In this case, "reply" really means "re-send," so we
1859 // target the original recipients. This works as expected even
1860 // if the user sent the original message to themselves.
1861 for (String address : inToAddresses) {
1862 if (!recipientMatchesThisAccount(address)) {
1863 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08001864 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001865 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001866 }
1867 return toAddresses;
1868 }
1869
Scott Kennedyff8553f2013-04-05 20:57:44 -07001870 private void addRecipients(final Set<String> recipients, final String[] addresses) {
1871 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001872 // Do not add this account, or any of its custom from addresses, to
1873 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001874 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08001875 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001876 recipients.add(email.replace("\"\"", ""));
1877 }
1878 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001879 }
1880
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001881 /**
1882 * A recipient matches this account if it has the same address as the
1883 * currently selected account OR one of the custom from addresses associated
1884 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001885 * @param recipientAddress address we are comparing with the currently selected account
1886 * @return
1887 */
mindyp5ee5d692012-11-19 16:02:16 -08001888 protected boolean recipientMatchesThisAccount(String recipientAddress) {
1889 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08001890 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001891 }
1892
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001893 /**
1894 * Returns a formatted subject string with the appropriate prefix for the action type.
1895 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
1896 */
1897 public static String buildFormattedSubject(Resources res, String subject, int action) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001898 String prefix;
1899 String correctedSubject = null;
1900 if (action == ComposeActivity.COMPOSE) {
1901 prefix = "";
1902 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001903 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001904 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001905 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001906 }
1907
1908 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07001909 if (!TextUtils.isEmpty(subject)
1910 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001911 correctedSubject = subject;
1912 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001913 correctedSubject = String.format(
1914 res.getString(R.string.formatted_subject), prefix, subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001915 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001916
1917 return correctedSubject;
1918 }
1919
1920 private void setSubject(Message refMessage, int action) {
1921 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001922 }
1923
Mindy Pereira818143e2012-01-11 13:59:49 -08001924 private void initRecipients() {
1925 setupRecipients(mTo);
1926 setupRecipients(mCc);
1927 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001928 }
1929
Mindy Pereira818143e2012-01-11 13:59:49 -08001930 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001931 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001932 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001933 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001934 int offset = accountName.indexOf("@") + 1;
1935 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001936 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001937 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001938 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001939 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001940 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001941 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001942 }
1943
1944 @Override
1945 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001946 final int id = v.getId();
1947 if (id == R.id.add_cc_bcc) {
1948 // Verify that cc/ bcc aren't showing.
1949 // Animate in cc/bcc.
1950 showCcBccViews();
1951 } else if (id == R.id.add_photo_attachment) {
1952 doAttach(MIME_TYPE_PHOTO);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001953 }
1954 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001955
1956 @Override
1957 public boolean onCreateOptionsMenu(Menu menu) {
1958 super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07001959 // Don't render any menu items when there are no accounts.
1960 if (mAccounts == null || mAccounts.length == 0) {
1961 return true;
1962 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001963 MenuInflater inflater = getMenuInflater();
1964 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08001965
1966 /*
1967 * Start save in the correct enabled state.
1968 * 1) If a user launches compose from within gmail, save is disabled
1969 * until they add something, at which point, save is enabled, auto save
1970 * on exit; if the user empties everything, save is disabled, exiting does not
1971 * auto-save
1972 * 2) if a user replies/ reply all/ forwards from within gmail, save is
1973 * disabled until they change something, at which point, save is
1974 * enabled, auto save on exit; if the user empties everything, save is
1975 * disabled, exiting does not auto-save.
1976 * 3) If a user launches compose from another application and something
1977 * gets populated (attachments, recipients, body, subject, etc), save is
1978 * enabled, auto save on exit; if the user empties everything, save is
1979 * disabled, exiting does not auto-save
1980 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08001981 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08001982 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07001983 enableSave(mInnerSavedState != null ?
1984 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08001985 : (Intent.ACTION_SEND.equals(action)
1986 || Intent.ACTION_SEND_MULTIPLE.equals(action)
1987 || Intent.ACTION_SENDTO.equals(action)
1988 || shouldSave()));
1989
Mindy Pereira82cc5662012-01-09 17:29:30 -08001990 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001991 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1992 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1993 if (helpItem != null) {
1994 helpItem.setVisible(mAccount != null
1995 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1996 }
1997 if (sendFeedbackItem != null) {
1998 sendFeedbackItem.setVisible(mAccount != null
1999 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2000 }
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002001
2002 // Only show attach file on K. Sigh.
2003 menu.findItem(R.id.add_file_attachment).setVisible(Utils.isRunningKitkatOrLater());
2004
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002005 return true;
2006 }
2007
2008 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002009 public boolean onPrepareOptionsMenu(Menu menu) {
2010 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08002011 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002012 // Its possible there is a menu item OR a button.
2013 boolean ccFieldVisible = mCc.isShown();
2014 boolean bccFieldVisible = mBcc.isShown();
2015 if (!ccFieldVisible || !bccFieldVisible) {
2016 ccBcc.setVisible(true);
2017 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
2018 : R.string.add_bcc_label));
2019 } else {
2020 ccBcc.setVisible(false);
2021 }
2022 }
2023 return true;
2024 }
2025
2026 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002027 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002028 final int id = item.getItemId();
Andy Huangdc97bf42013-08-15 16:52:45 -07002029
2030 Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id, null, 0);
2031
Mindy Pereira75f66632012-01-11 11:42:02 -08002032 boolean handled = true;
Andrew Sapperstein5cb71802013-10-01 18:31:20 -07002033 if (id == R.id.add_file_attachment) {
2034 doAttach(MIME_TYPE_ALL);
2035 } else if (id == R.id.add_photo_attachment) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07002036 doAttach(MIME_TYPE_PHOTO);
2037 } else if (id == R.id.add_video_attachment) {
2038 doAttach(MIME_TYPE_VIDEO);
2039 } else if (id == R.id.add_cc_bcc) {
2040 showCcBccViews();
2041 } else if (id == R.id.save) {
2042 doSave(true);
2043 } else if (id == R.id.send) {
2044 doSend();
2045 } else if (id == R.id.discard) {
2046 doDiscard();
2047 } else if (id == R.id.settings) {
2048 Utils.showSettings(this, mAccount);
2049 } else if (id == android.R.id.home) {
2050 onAppUpPressed();
2051 } else if (id == R.id.help_info_menu_item) {
2052 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2053 } else if (id == R.id.feedback_menu_item) {
2054 Utils.sendFeedback(this, mAccount, false);
2055 } else {
2056 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08002057 }
2058 return !handled ? super.onOptionsItemSelected(item) : handled;
2059 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002060
Mindy Pereirab199d172012-08-13 11:04:03 -07002061 @Override
2062 public void onBackPressed() {
2063 // If we are showing the wait fragment, just exit.
2064 if (getWaitFragment() != null) {
2065 finish();
2066 } else {
2067 super.onBackPressed();
2068 }
2069 }
2070
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07002071 /**
2072 * Carries out the "up" action in the action bar.
2073 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002074 private void onAppUpPressed() {
2075 if (mLaunchedFromEmail) {
2076 // If this was started from Gmail, simply treat app up as the system back button, so
2077 // that the last view is restored.
2078 onBackPressed();
2079 return;
2080 }
2081
2082 // Fire the main activity to ensure it launches the "top" screen of mail.
2083 // Since the main Activity is singleTask, it should revive that task if it was already
2084 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07002085 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07002086 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2087 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2088 startActivity(mailIntent);
2089 finish();
2090 }
2091
Mindy Pereira33fe9082012-01-09 16:24:30 -08002092 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07002093 sendOrSaveWithSanityChecks(false, true, false, false);
Andy Huangdc97bf42013-08-15 16:52:45 -07002094 logSendOrSave(false /* save */);
2095 mPerformedSendOrDiscard = true;
Mindy Pereira33fe9082012-01-09 16:24:30 -08002096 }
2097
Mindy Pereira48e31b02012-05-30 13:12:24 -07002098 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07002099 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07002100 }
2101
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002102 @VisibleForTesting
2103 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002104 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002105 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2106 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002107 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2108 }
2109
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002110 @VisibleForTesting
2111 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002112 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002113 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002114 public final SendOrSaveCallback mSendOrSaveCallback;
2115 @VisibleForTesting
2116 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002117 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002118
2119 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002120 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002121 mContext = context;
2122 mSendOrSaveCallback = callback;
2123 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002124 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002125 }
2126
2127 @Override
2128 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002129 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002130
Mindy Pereira92551d02012-04-05 11:31:12 -07002131 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002132 Message message = mSendOrSaveCallback.getMessage();
2133 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002134 // If a previous draft has been saved, in an account that is different
2135 // than what the user wants to send from, remove the old draft, and treat this
2136 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002137 if (mExistingDraftAccount != null
2138 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002139 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2140 ContentResolver resolver = mContext.getContentResolver();
2141 ContentValues values = new ContentValues();
2142 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002143 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2144 new ContentProviderTask.UpdateTask()
2145 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2146 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002147 } else {
2148 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002149 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002150 // reset messageId to 0, so a new message will be created
2151 messageId = UIProvider.INVALID_MESSAGE_ID;
2152 }
2153 }
2154
2155 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002156 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002157
2158 if (!sendOrSaveMessage.mSave) {
Tony Mantler9f324232013-08-08 14:24:30 -07002159 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002160 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Tony Mantler9f324232013-08-08 14:24:30 -07002161 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002162 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Tony Mantler9f324232013-08-08 14:24:30 -07002163 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002164 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2165 }
2166 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2167 }
2168
Tony Mantler9f324232013-08-08 14:24:30 -07002169 private static void incrementRecipientsTimesContacted(final Context context,
2170 final String addressString) {
2171 if (TextUtils.isEmpty(addressString)) {
2172 return;
2173 }
2174 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2175 final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
2176 for (int i = 0; i < tokens.length;i++) {
2177 recipients.add(tokens[i].getAddress());
2178 }
2179 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
2180 statsUpdater.updateWithAddress(recipients);
2181 }
2182
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002183 /**
2184 * Send or Save a message.
2185 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002186 private void sendOrSaveMessage(final long messageIdToSave,
2187 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002188 final ContentResolver resolver = mContext.getContentResolver();
2189 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2190
2191 final String accountMethod = sendOrSaveMessage.mSave ?
2192 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2193 UIProvider.AccountCallMethods.SEND_MESSAGE;
2194
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002195 try {
2196 if (updateExistingMessage) {
2197 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002198
Paul Westbrook013a23c2013-02-22 10:37:41 -08002199 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002200 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002201 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002202 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002203 final Bundle result = callAccountSendSaveMethod(resolver,
2204 selectedAccount.account, accountMethod, sendOrSaveMessage);
2205 if (result != null) {
2206 // If a non-null value was returned, then the provider handled the call
2207 // method
2208 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002209 }
2210 if (sendOrSaveMessage.mSave && messageUri != null) {
2211 final Cursor messageCursor = resolver.query(messageUri,
2212 UIProvider.MESSAGE_PROJECTION, null, null, null);
2213 if (messageCursor != null) {
2214 try {
2215 if (messageCursor.moveToFirst()) {
2216 // Broadcast notification that a new message has
2217 // been allocated
2218 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2219 new Message(messageCursor));
2220 }
2221 } finally {
2222 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002223 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002224 }
2225 }
2226 }
2227 } finally {
2228 // Close any opened file descriptors
2229 closeOpenedAttachmentFds(sendOrSaveMessage);
2230 }
2231 }
2232
Scott Kennedyff8553f2013-04-05 20:57:44 -07002233 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002234 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2235 if (openedFds != null) {
2236 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002237 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002238 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2239 if (fd != null) {
2240 try {
2241 fd.close();
2242 } catch (IOException e) {
2243 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002244 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002245 }
2246 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002247 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002248 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002249
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002250 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002251 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002252 *
2253 * If this was successful, this method will return an non-null Bundle instance
2254 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002255 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2256 final Account account, final String method,
2257 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002258 // Copy all of the values from the content values to the bundle
2259 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2260 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2261
2262 for (Entry<String, Object> entry : valueSet) {
2263 final Object entryValue = entry.getValue();
2264 final String key = entry.getKey();
2265 if (entryValue instanceof String) {
2266 methodExtras.putString(key, (String)entryValue);
2267 } else if (entryValue instanceof Boolean) {
2268 methodExtras.putBoolean(key, (Boolean)entryValue);
2269 } else if (entryValue instanceof Integer) {
2270 methodExtras.putInt(key, (Integer)entryValue);
2271 } else if (entryValue instanceof Long) {
2272 methodExtras.putLong(key, (Long)entryValue);
2273 } else {
2274 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2275 entryValue.getClass().getName());
2276 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002277 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002278
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002279 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2280 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2281 if (fdMap != null) {
2282 methodExtras.putParcelable(
2283 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2284 }
2285
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002286 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002287 }
2288 }
2289
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002290 @VisibleForTesting
2291 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002292 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002293 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002294 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002295 @VisibleForTesting
2296 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002297 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002298 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002299
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002300 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2301 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002302 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002303 mValues = values;
2304 mRefMessageId = refMessageId;
2305 mSave = save;
2306 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002307
2308 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002309 }
2310
2311 int requestId() {
2312 return mRequestId;
2313 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002314
2315 Bundle attachmentFds() {
2316 return mAttachmentFds;
2317 }
2318
2319 /**
2320 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2321 * called before the ComposeActivity finishes.
2322 * Note: The caller is responsible for closing these file descriptors.
2323 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002324 private static Bundle initializeAttachmentFds(final Context context,
2325 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002326 if (attachments == null || attachments.size() == 0) {
2327 return null;
2328 }
2329
2330 final Bundle result = new Bundle(attachments.size());
2331 final ContentResolver resolver = context.getContentResolver();
2332
2333 for (Attachment attachment : attachments) {
2334 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2335 continue;
2336 }
2337
2338 ParcelFileDescriptor fileDescriptor;
2339 try {
2340 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2341 } catch (FileNotFoundException e) {
2342 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2343 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002344 } catch (SecurityException e) {
2345 // We have encountered a security exception when attempting to open the file
2346 // specified by the content uri. If the attachment has been cached, this
2347 // isn't a problem, as even through the original permission may have been
2348 // revoked, we have cached the file. This will happen when saving/sending
2349 // a previously saved draft.
2350 // TODO(markwei): Expose whether the attachment has been cached through the
2351 // attachment object. This would allow us to limit when the log is made, as
2352 // if the attachment has been cached, this really isn't an error
2353 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2354 // Just set the file descriptor to null, as the underlying provider needs
2355 // to handle the file descriptor not being set.
2356 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002357 }
2358
2359 if (fileDescriptor != null) {
2360 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2361 }
2362 }
2363
2364 return result;
2365 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002366 }
2367
2368 /**
2369 * Get the to recipients.
2370 */
2371 public String[] getToAddresses() {
2372 return getAddressesFromList(mTo);
2373 }
2374
2375 /**
2376 * Get the cc recipients.
2377 */
2378 public String[] getCcAddresses() {
2379 return getAddressesFromList(mCc);
2380 }
2381
2382 /**
2383 * Get the bcc recipients.
2384 */
2385 public String[] getBccAddresses() {
2386 return getAddressesFromList(mBcc);
2387 }
2388
2389 public String[] getAddressesFromList(RecipientEditTextView list) {
2390 if (list == null) {
2391 return new String[0];
2392 }
2393 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2394 int count = tokens.length;
2395 String[] result = new String[count];
2396 for (int i = 0; i < count; i++) {
2397 result[i] = tokens[i].toString();
2398 }
2399 return result;
2400 }
2401
2402 /**
2403 * Check for invalid email addresses.
2404 * @param to String array of email addresses to check.
2405 * @param wrongEmailsOut Emails addresses that were invalid.
2406 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002407 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002408 if (mValidator == null) {
2409 return;
2410 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002411 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002412 if (!mValidator.isValid(email)) {
2413 wrongEmailsOut.add(email);
2414 }
2415 }
2416 }
2417
Tony Mantler2558b502013-07-09 10:53:34 -07002418 public static class RecipientErrorDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002419 // Public no-args constructor needed for fragment re-instantiation
2420 public RecipientErrorDialogFragment() {}
2421
Tony Mantler2558b502013-07-09 10:53:34 -07002422 public static RecipientErrorDialogFragment newInstance(final String message) {
2423 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2424 final Bundle args = new Bundle(1);
2425 args.putString("message", message);
2426 frag.setArguments(args);
2427 return frag;
2428 }
2429
2430 @Override
2431 public Dialog onCreateDialog(Bundle savedInstanceState) {
2432 final String message = getArguments().getString("message");
2433 return new AlertDialog.Builder(getActivity()).setMessage(message).setTitle(
2434 R.string.recipient_error_dialog_title)
2435 .setIconAttribute(android.R.attr.alertDialogIcon)
2436 .setPositiveButton(
2437 R.string.ok, new Dialog.OnClickListener() {
2438 @Override
2439 public void onClick(DialogInterface dialog, int which) {
2440 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2441 }
2442 }).create();
2443 }
2444 }
2445
2446 private void finishRecipientErrorDialog() {
2447 // after the user dismisses the recipient error
2448 // dialog we want to make sure to refocus the
2449 // recipient to field so they can fix the issue
2450 // easily
2451 if (mTo != null) {
2452 mTo.requestFocus();
2453 }
2454 }
2455
Mindy Pereira82cc5662012-01-09 17:29:30 -08002456 /**
2457 * Show an error because the user has entered an invalid recipient.
2458 * @param message
2459 */
Tony Mantler2558b502013-07-09 10:53:34 -07002460 private void showRecipientErrorDialog(final String message) {
2461 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2462 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002463 }
2464
2465 /**
2466 * Update the state of the UI based on whether or not the current draft
2467 * needs to be saved and the message is not empty.
2468 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002469 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002470 if (mSave != null) {
2471 mSave.setEnabled((shouldSave() && !isBlank()));
2472 }
2473 }
2474
2475 /**
2476 * Returns true if we need to save the current draft.
2477 */
2478 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002479 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002480 // The message should only be saved if:
2481 // It hasn't been sent AND
2482 // Some text has been added to the message OR
2483 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002484 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002485 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002486 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002487 }
2488 }
2489
2490 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002491 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002492 * @return boolean
2493 */
2494 public boolean isBlank() {
Alice Yanga49b6842013-08-23 10:36:18 -07002495 // Need to check for null since isBlank() can be called from onPause()
2496 // before findViews() is called
2497 if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2498 mAttachmentsView == null) {
2499 LogUtils.w(LOG_TAG, "null views in isBlank check");
2500 return true;
2501 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002502 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002503 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2504 mBodyView.getText().toString()) == 0)
2505 && mTo.length() == 0
2506 && mCc.length() == 0 && mBcc.length() == 0
2507 && mAttachmentsView.getAttachments().size() == 0;
2508 }
2509
2510 @VisibleForTesting
2511 protected int getSignatureStartPosition(String signature, String bodyText) {
2512 int startPos = -1;
2513
2514 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2515 return startPos;
2516 }
2517
2518 int bodyLength = bodyText.length();
2519 int signatureLength = signature.length();
2520 String printableVersion = convertToPrintableSignature(signature);
2521 int printableLength = printableVersion.length();
2522
2523 if (bodyLength >= printableLength
2524 && bodyText.substring(bodyLength - printableLength)
2525 .equals(printableVersion)) {
2526 startPos = bodyLength - printableLength;
2527 } else if (bodyLength >= signatureLength
2528 && bodyText.substring(bodyLength - signatureLength)
2529 .equals(signature)) {
2530 startPos = bodyLength - signatureLength;
2531 }
2532 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002533 }
2534
2535 /**
2536 * Allows any changes made by the user to be ignored. Called when the user
2537 * decides to discard a draft.
2538 */
2539 private void discardChanges() {
2540 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002541 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002542 mReplyFromChanged = false;
2543 }
2544
2545 /**
Mindy Pereira181df782012-03-01 13:32:44 -08002546 * @param save
2547 * @param showToast
2548 * @return Whether the send or save succeeded.
2549 */
2550 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002551 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002552 if (mAccounts == null || mAccount == null) {
2553 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002554 if (autoSend) {
2555 finish();
2556 }
Mark Wei009b3712012-10-18 18:07:50 -07002557 return false;
2558 }
2559
Scott Kennedyff8553f2013-04-05 20:57:44 -07002560 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002561 if (orientationChanged) {
2562 to = cc = bcc = new String[0];
2563 } else {
2564 to = getToAddresses();
2565 cc = getCcAddresses();
2566 bcc = getBccAddresses();
2567 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002568
Mindy Pereira181df782012-03-01 13:32:44 -08002569 // Don't let the user send to nobody (but it's okay to save a message
2570 // with no recipients)
2571 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2572 showRecipientErrorDialog(getString(R.string.recipient_needed));
2573 return false;
2574 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002575
Mindy Pereira181df782012-03-01 13:32:44 -08002576 List<String> wrongEmails = new ArrayList<String>();
2577 if (!save) {
2578 checkInvalidEmails(to, wrongEmails);
2579 checkInvalidEmails(cc, wrongEmails);
2580 checkInvalidEmails(bcc, wrongEmails);
2581 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002582
Mindy Pereira181df782012-03-01 13:32:44 -08002583 // Don't let the user send an email with invalid recipients
2584 if (wrongEmails.size() > 0) {
2585 String errorText = String.format(getString(R.string.invalid_recipient),
2586 wrongEmails.get(0));
2587 showRecipientErrorDialog(errorText);
2588 return false;
2589 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002590
Mindy Pereira181df782012-03-01 13:32:44 -08002591 // Show a warning before sending only if there are no attachments.
2592 if (!save) {
2593 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2594 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002595 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002596
Mindy Pereira181df782012-03-01 13:32:44 -08002597 // A warning about an empty body may not be warranted when
2598 // forwarding mails, since a common use case is to forward
2599 // quoted text and not append any more text.
2600 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002601
Mindy Pereira181df782012-03-01 13:32:44 -08002602 // When we bring up a dialog warning the user about a send,
2603 // assume that they accept sending the message. If they do not,
2604 // the dialog listener is required to enable sending again.
2605 if (warnAboutEmptySubject) {
Tony Mantler2558b502013-07-09 10:53:34 -07002606 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, save,
2607 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002608 return true;
2609 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002610
Mindy Pereira181df782012-03-01 13:32:44 -08002611 if (warnAboutEmptyBody) {
Tony Mantler2558b502013-07-09 10:53:34 -07002612 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, save,
2613 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002614 return true;
2615 }
2616 }
2617 // Ask for confirmation to send (if always required)
2618 if (showSendConfirmation()) {
Tony Mantler2558b502013-07-09 10:53:34 -07002619 showSendConfirmDialog(R.string.confirm_send_message, save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002620 return true;
2621 }
2622 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002623
Tony Mantler2558b502013-07-09 10:53:34 -07002624 sendOrSave(save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002625 return true;
2626 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002627
Mindy Pereira181df782012-03-01 13:32:44 -08002628 /**
2629 * Returns a boolean indicating whether warnings should be shown for empty
2630 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002631 *
Mindy Pereira181df782012-03-01 13:32:44 -08002632 * @return True if a warning should be shown for empty text fields
2633 */
2634 protected boolean showEmptyTextWarnings() {
2635 return mAttachmentsView.getAttachments().size() == 0;
2636 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002637
Mindy Pereira181df782012-03-01 13:32:44 -08002638 /**
2639 * Returns a boolean indicating whether the user should confirm each send
2640 *
2641 * @return True if a warning should be on each send
2642 */
2643 protected boolean showSendConfirmation() {
2644 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2645 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002646
Tony Mantler2558b502013-07-09 10:53:34 -07002647 public static class SendConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07002648 // Public no-args constructor needed for fragment re-instantiation
2649 public SendConfirmDialogFragment() {}
2650
Tony Mantler2558b502013-07-09 10:53:34 -07002651 public static SendConfirmDialogFragment newInstance(final int messageId,
2652 final boolean save, final boolean showToast) {
2653 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2654 final Bundle args = new Bundle(3);
2655 args.putInt("messageId", messageId);
2656 args.putBoolean("save", save);
2657 args.putBoolean("showToast", showToast);
2658 frag.setArguments(args);
2659 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002660 }
Tony Mantler2558b502013-07-09 10:53:34 -07002661
2662 @Override
2663 public Dialog onCreateDialog(Bundle savedInstanceState) {
2664 final int messageId = getArguments().getInt("messageId");
2665 final boolean save = getArguments().getBoolean("save");
2666 final boolean showToast = getArguments().getBoolean("showToast");
2667
2668 return new AlertDialog.Builder(getActivity())
2669 .setMessage(messageId)
2670 .setTitle(R.string.confirm_send_title)
2671 .setIconAttribute(android.R.attr.alertDialogIcon)
2672 .setPositiveButton(R.string.send,
2673 new DialogInterface.OnClickListener() {
Scott Kennedyaa27bc02013-08-02 08:47:26 -07002674 @Override
Tony Mantler2558b502013-07-09 10:53:34 -07002675 public void onClick(DialogInterface dialog, int whichButton) {
2676 ((ComposeActivity)getActivity()).finishSendConfirmDialog(save,
2677 showToast);
2678 }
2679 })
2680 .create();
2681 }
2682 }
2683
2684 private void finishSendConfirmDialog(final boolean save, final boolean showToast) {
2685 sendOrSave(save, showToast);
2686 }
2687
2688 private void showSendConfirmDialog(final int messageId, final boolean save,
2689 final boolean showToast) {
2690 final DialogFragment frag = SendConfirmDialogFragment.newInstance(messageId, save,
2691 showToast);
2692 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08002693 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002694
Mindy Pereira181df782012-03-01 13:32:44 -08002695 /**
2696 * Returns whether the ComposeArea believes there is any text in the body of
2697 * the composition. TODO: When ComposeArea controls the Body as well, add
2698 * that here.
2699 */
2700 public boolean isBodyEmpty() {
2701 return !mQuotedTextView.isTextIncluded();
2702 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002703
Mindy Pereira181df782012-03-01 13:32:44 -08002704 /**
2705 * Test to see if the subject is empty.
2706 *
2707 * @return boolean.
2708 */
2709 // TODO: this will likely go away when composeArea.focus() is implemented
2710 // after all the widget control is moved over.
2711 public boolean isSubjectEmpty() {
2712 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2713 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002714
Mindy Pereira181df782012-03-01 13:32:44 -08002715 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002716 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07002717 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08002718 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
Scott Kennedy60847252013-08-15 15:55:42 -07002719 ReplyFromAccount draftAccount, final ContentValues extraValues) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08002720 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002721
Paul Westbrookb4931c62013-01-14 17:51:18 -08002722 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07002723
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002724 MessageModification.putToAddresses(values, message.getToAddresses());
2725 MessageModification.putCcAddresses(values, message.getCcAddresses());
2726 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002727
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002728 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07002729
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002730 MessageModification.putSubject(values, message.subject);
Paul Westbrookb4931c62013-01-14 17:51:18 -08002731 // Make sure to remove only the composing spans from the Spannable before saving.
2732 final String htmlBody = Html.toHtml(removeComposingSpans(body));
Paul Westbrook05b92b82012-04-20 13:29:37 -07002733
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002734 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2735 StringBuilder fullBody = new StringBuilder(htmlBody);
2736 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002737 // HTML gets converted to text for now
2738 final String text = quotedText.toString();
2739 if (QuotedTextView.containsQuotedText(text)) {
2740 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07002741 final int quoteStartPos = fullBody.length() + pos;
2742 fullBody.append(text);
2743 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07002744 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07002745 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002746 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002747 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2748 // This shouldn't happen, but just use what we have,
2749 // and don't do server-side expansion
2750 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002751 }
2752 }
Mindy Pereira002ff522012-05-30 10:31:26 -07002753 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07002754 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002755 if (refMessage != null) {
2756 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2757 MessageModification.putBodyHtml(values, fullBody.toString());
2758 }
2759 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08002760 MessageModification.putBody(values,
2761 Utils.convertHtmlToPlainText(fullBody.toString()).toString());
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002762 }
2763 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002764 MessageModification.putBodyHtml(values, fullBody.toString());
mindypc59dd822012-11-13 10:56:21 -08002765 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2766 .toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002767 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002768 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002769 if (!TextUtils.isEmpty(refMessageId)) {
2770 MessageModification.putRefMessageId(values, refMessageId);
2771 }
Scott Kennedy60847252013-08-15 15:55:42 -07002772 if (extraValues != null) {
2773 values.putAll(extraValues);
2774 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002775 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2776 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08002777 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2778 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002779
Mindy Pereira181df782012-03-01 13:32:44 -08002780 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08002781 // Do the send/save action on the specified handler to avoid possible
2782 // ANRs
2783 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002784
Mindy Pereira181df782012-03-01 13:32:44 -08002785 return sendOrSaveMessage.requestId();
2786 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002787
Paul Westbrookb4931c62013-01-14 17:51:18 -08002788 /**
2789 * Removes any composing spans from the specified string. This will create a new
2790 * SpannableString instance, as to not modify the behavior of the EditText view.
2791 */
2792 private static SpannableString removeComposingSpans(Spanned body) {
2793 final SpannableString messageBody = new SpannableString(body);
2794 BaseInputConnection.removeComposingSpans(messageBody);
2795 return messageBody;
2796 }
2797
Mindy Pereira002ff522012-05-30 10:31:26 -07002798 private static int getDraftType(int mode) {
2799 int draftType = -1;
2800 switch (mode) {
2801 case ComposeActivity.COMPOSE:
2802 draftType = DraftType.COMPOSE;
2803 break;
2804 case ComposeActivity.REPLY:
2805 draftType = DraftType.REPLY;
2806 break;
2807 case ComposeActivity.REPLY_ALL:
2808 draftType = DraftType.REPLY_ALL;
2809 break;
2810 case ComposeActivity.FORWARD:
2811 draftType = DraftType.FORWARD;
2812 break;
2813 }
2814 return draftType;
2815 }
2816
Tony Mantler2558b502013-07-09 10:53:34 -07002817 private void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08002818 // Check if user is a monkey. Monkeys can compose and hit send
2819 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002820 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002821 return;
2822 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002823
Tony Mantler2558b502013-07-09 10:53:34 -07002824 final Spanned body = mBodyView.getEditableText();
2825
Mindy Pereira181df782012-03-01 13:32:44 -08002826 SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07002827 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08002828 private int mRestoredRequestId;
2829
Marc Blank0bbc8582012-04-23 15:07:57 -07002830 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002831 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002832 synchronized (mActiveTasks) {
2833 int numTasks = mActiveTasks.size();
2834 if (numTasks == 0) {
2835 // Start service so we won't be killed if this app is
2836 // put in the background.
2837 startService(new Intent(ComposeActivity.this, EmptyService.class));
2838 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002839
Mindy Pereira181df782012-03-01 13:32:44 -08002840 mActiveTasks.add(sendOrSaveTask);
2841 }
2842 if (sTestSendOrSaveCallback != null) {
2843 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2844 }
2845 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002846
Marc Blank0bbc8582012-04-23 15:07:57 -07002847 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002848 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2849 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002850 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08002851 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002852 mDraftId = message.id;
2853 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002854 if (sRequestMessageIdMap != null) {
2855 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2856 }
Mindy Pereira181df782012-03-01 13:32:44 -08002857 // Cache request message map, in case the process is killed
2858 saveRequestMap();
2859 }
2860 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002861 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002862 }
2863 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002864
Marc Blank0bbc8582012-04-23 15:07:57 -07002865 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002866 public Message getMessage() {
2867 synchronized (mDraftLock) {
2868 return mDraft;
2869 }
2870 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002871
Marc Blank0bbc8582012-04-23 15:07:57 -07002872 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002873 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07002874 // Update the last sent from account.
2875 if (mAccount != null) {
2876 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2877 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002878 if (success) {
2879 // Successfully sent or saved so reset change markers
2880 discardChanges();
2881 } else {
2882 // A failure happened with saving/sending the draft
2883 // TODO(pwestbro): add a better string that should be used
2884 // when failing to send or save
2885 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2886 .show();
2887 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002888
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002889 int numTasks;
2890 synchronized (mActiveTasks) {
2891 // Remove the task from the list of active tasks
2892 mActiveTasks.remove(task);
2893 numTasks = mActiveTasks.size();
2894 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002895
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002896 if (numTasks == 0) {
2897 // Stop service so we can be killed.
2898 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2899 }
2900 if (sTestSendOrSaveCallback != null) {
2901 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2902 }
2903 }
Mindy Pereira181df782012-03-01 13:32:44 -08002904 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002905
Tony Mantler1e05a1e2013-08-12 16:44:26 -07002906 setAccount(mReplyFromAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002907
Mindy Pereira181df782012-03-01 13:32:44 -08002908 if (mSendSaveTaskHandler == null) {
2909 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2910 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002911
Mindy Pereira181df782012-03-01 13:32:44 -08002912 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2913 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002914
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002915 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002916 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2917 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Scott Kennedy60847252013-08-15 15:55:42 -07002918 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount, mExtraValues);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002919
Mindy Pereira181df782012-03-01 13:32:44 -08002920 // Don't display the toast if the user is just changing the orientation,
2921 // but we still need to save the draft to the cursor because this is how we restore
2922 // the attachments when the configuration change completes.
2923 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2924 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2925 Toast.LENGTH_LONG).show();
2926 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002927
Mindy Pereira181df782012-03-01 13:32:44 -08002928 // Need to update variables here because the send or save completes
2929 // asynchronously even though the toast shows right away.
2930 discardChanges();
2931 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002932
Mindy Pereira181df782012-03-01 13:32:44 -08002933 // If we are sending, finish the activity
2934 if (!save) {
2935 finish();
2936 }
2937 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002938
Mindy Pereira181df782012-03-01 13:32:44 -08002939 /**
2940 * Save the state of the request messageid map. This allows for the Gmail
2941 * process to be killed, but and still allow for ComposeActivity instances
2942 * to be recreated correctly.
2943 */
2944 private void saveRequestMap() {
2945 // TODO: store the request map in user preferences.
2946 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002947
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002948 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002949 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2950 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002951 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein05089f32013-10-01 17:00:03 -07002952 i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002953 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08002954 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002955 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2956 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002957 }
2958
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002959 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002960 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002961 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07002962 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002963 }
2964 }
2965
Andy Huang4fe0af82013-08-20 17:24:51 -07002966 private static String getActionString(int action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07002967 final String msgType;
Andy Huang4fe0af82013-08-20 17:24:51 -07002968 switch (action) {
Andy Huangdc97bf42013-08-15 16:52:45 -07002969 case COMPOSE:
2970 msgType = "new_message";
2971 break;
2972 case REPLY:
2973 msgType = "reply";
2974 break;
2975 case REPLY_ALL:
2976 msgType = "reply_all";
2977 break;
2978 case FORWARD:
2979 msgType = "forward";
2980 break;
2981 default:
2982 msgType = "unknown";
2983 break;
2984 }
Andy Huang4fe0af82013-08-20 17:24:51 -07002985 return msgType;
2986 }
2987
2988 private void logSendOrSave(boolean save) {
2989 if (!Analytics.isLoggable() || mAttachmentsView == null) {
2990 return;
2991 }
2992
2993 final String category = (save) ? "message_save" : "message_send";
2994 final int attachmentCount = getAttachments().size();
2995 final String msgType = getActionString(mComposeMode);
Andy Huangdc97bf42013-08-15 16:52:45 -07002996 final String label;
2997 final long value;
2998 if (mComposeMode == COMPOSE) {
2999 label = Integer.toString(attachmentCount);
3000 value = attachmentCount;
3001 } else {
3002 label = null;
3003 value = 0;
3004 }
3005 Analytics.getInstance().sendEvent(category, msgType, label, value);
3006 }
3007
Mindy Pereira326c6602012-01-04 15:32:42 -08003008 @Override
3009 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003010 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08003011 if (position == ComposeActivity.REPLY) {
3012 mComposeMode = ComposeActivity.REPLY;
3013 } else if (position == ComposeActivity.REPLY_ALL) {
3014 mComposeMode = ComposeActivity.REPLY_ALL;
3015 } else if (position == ComposeActivity.FORWARD) {
3016 mComposeMode = ComposeActivity.FORWARD;
3017 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003018 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003019 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08003020 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08003021 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003022 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07003023 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003024 boolean showCc = false;
3025 boolean showBcc = false;
3026 if (mDraft != null) {
3027 // Following desktop behavior, if the user has added a BCC
3028 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003029 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07003030 // Use the draft to determine what to populate.
3031 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08003032 showCc = showBcc
3033 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08003034 }
3035 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07003036 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08003037 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07003038 }
3039 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08003040 }
Mindy Pereiraef388302012-06-18 19:07:44 -07003041 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07003042 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08003043 return true;
3044 }
3045
Mindy Pereirab3112a22012-06-20 12:10:03 -07003046 @VisibleForTesting
3047 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08003048 // When switching between reply, reply all, forward,
3049 // follow the behavior of webview.
3050 // The contents of the following fields are cleared
3051 // so that they can be populated directly from the
3052 // ref message:
3053 // 1) Any recipient fields
3054 // 2) The subject
3055 mTo.setText("");
3056 mCc.setText("");
3057 mBcc.setText("");
3058 // Any edits to the subject are replaced with the original subject.
3059 mSubject.setText("");
3060
3061 // Any changes to the contents of the following fields are kept:
3062 // 1) Body
3063 // 2) Attachments
3064 // If the user made changes to attachments, keep their changes.
3065 if (!mAttachmentsChanged) {
3066 mAttachmentsView.deleteAllAttachments();
3067 }
3068 }
3069
Mindy Pereira326c6602012-01-04 15:32:42 -08003070 private class ComposeModeAdapter extends ArrayAdapter<String> {
3071
3072 private LayoutInflater mInflater;
3073
3074 public ComposeModeAdapter(Context context) {
3075 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3076 .getStringArray(R.array.compose_modes));
3077 }
3078
3079 private LayoutInflater getInflater() {
3080 if (mInflater == null) {
3081 mInflater = LayoutInflater.from(getContext());
3082 }
3083 return mInflater;
3084 }
3085
3086 @Override
3087 public View getView(int position, View convertView, ViewGroup parent) {
3088 if (convertView == null) {
3089 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3090 }
3091 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3092 return super.getView(position, convertView, parent);
3093 }
3094 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003095
3096 @Override
3097 public void onRespondInline(String text) {
3098 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07003099 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08003100 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08003101 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08003102 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08003103 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003104 }
3105
3106 /**
3107 * Append text to the body of the message. If there is no existing body
3108 * text, just sets the body to text.
3109 *
3110 * @param text
3111 * @param withSignature True to append a signature.
3112 */
3113 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003114 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003115 if (bodyText != null && bodyText.length() > 0) {
3116 bodyText.append(text);
3117 } else {
3118 setBody(text, withSignature);
3119 }
3120 }
3121
3122 /**
3123 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003124 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003125 * @param text
3126 * @param withSignature True to append a signature.
3127 */
3128 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003129 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003130 if (withSignature) {
3131 appendSignature();
3132 }
3133 }
3134
3135 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003136 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07003137 boolean hasFocus = mBodyView.hasFocus();
mindyp27083062012-11-15 09:02:01 -08003138 int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
3139 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003140 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08003141 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07003142 // Appending a signature does not count as changing text.
3143 mBodyView.removeTextChangedListener(this);
3144 mBodyView.append(convertToPrintableSignature(mSignature));
3145 mBodyView.addTextChangedListener(this);
3146 }
Mindy Pereira433b1982012-04-03 11:53:07 -07003147 if (hasFocus) {
3148 focusBody();
3149 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003150 }
3151 }
3152
3153 private String convertToPrintableSignature(String signature) {
3154 String signatureResource = getResources().getString(R.string.signature);
3155 if (signature == null) {
3156 signature = "";
3157 }
3158 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003159 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003160
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003161 @Override
3162 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003163 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3164 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003165 // Clear a signature, if there was one.
3166 mBodyView.removeTextChangedListener(this);
3167 String oldSignature = mSignature;
3168 String bodyText = getBody().getText().toString();
3169 if (!TextUtils.isEmpty(oldSignature)) {
3170 int pos = getSignatureStartPosition(oldSignature, bodyText);
3171 if (pos > -1) {
3172 mBodyView.setText(bodyText.substring(0, pos));
3173 }
3174 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003175 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003176 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003177 // TODO: handle discarding attachments when switching accounts.
3178 // Only enable save for this draft if there is any other content
3179 // in the message.
3180 if (!isBlank()) {
3181 enableSave(true);
3182 }
3183 mReplyFromChanged = true;
3184 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003185 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003186 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003187
3188 public void enableSave(boolean enabled) {
3189 if (mSave != null) {
3190 mSave.setEnabled(enabled);
3191 }
3192 }
3193
Tony Mantler2558b502013-07-09 10:53:34 -07003194 public static class DiscardConfirmDialogFragment extends DialogFragment {
Paul Westbrookf0ea4842013-08-13 16:41:18 -07003195 // Public no-args constructor needed for fragment re-instantiation
3196 public DiscardConfirmDialogFragment() {}
3197
Tony Mantler2558b502013-07-09 10:53:34 -07003198 @Override
3199 public Dialog onCreateDialog(Bundle savedInstanceState) {
3200 return new AlertDialog.Builder(getActivity())
3201 .setMessage(R.string.confirm_discard_text)
3202 .setPositiveButton(R.string.discard,
3203 new DialogInterface.OnClickListener() {
3204 @Override
3205 public void onClick(DialogInterface dialog, int which) {
3206 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3207 }
3208 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003209 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003210 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003211 }
3212 }
3213
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003214 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003215 final DialogFragment frag = new DiscardConfirmDialogFragment();
3216 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003217 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003218 /**
3219 * Effectively discard the current message.
3220 *
3221 * This method is either invoked from the menu or from the dialog
3222 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003223 */
Tony Mantler2558b502013-07-09 10:53:34 -07003224 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003225 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003226 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3227 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003228 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003229 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003230 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3231 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003232 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003233 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003234 // This is not strictly necessary (since we should not try to
3235 // save the draft after calling this) but it ensures that if we
3236 // do save again for some reason we make a new draft rather than
3237 // trying to resave an expunged draft.
3238 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3239 }
3240 }
3241
Tony Mantler2558b502013-07-09 10:53:34 -07003242 // Display a toast to let the user know
3243 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003244
3245 // This prevents the draft from being saved in onPause().
3246 discardChanges();
Andy Huangdc97bf42013-08-15 16:52:45 -07003247 mPerformedSendOrDiscard = true;
Mindy Pereira82cc5662012-01-09 17:29:30 -08003248 finish();
3249 }
3250
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003251 private void saveIfNeeded() {
3252 if (mAccount == null) {
3253 // We have not chosen an account yet so there's no way that we can save. This is ok,
3254 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3255 // user has not interacted with us yet and there is no real state to save.
3256 return;
3257 }
3258
3259 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003260 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003261 }
3262 }
3263
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003264 @Override
3265 public void onAttachmentDeleted() {
3266 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003267 // If we are showing any attachments, make sure we have an upper
3268 // divider.
3269 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003270 updateSaveUi();
3271 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003272
mindyp40882432012-09-06 11:07:40 -07003273 @Override
3274 public void onAttachmentAdded() {
3275 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3276 mAttachmentsView.focusLastAttachment();
3277 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003278
3279 /**
3280 * This is called any time one of our text fields changes.
3281 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003282 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003283 public void afterTextChanged(Editable s) {
3284 mTextChanged = true;
3285 updateSaveUi();
3286 }
3287
3288 @Override
3289 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3290 // Do nothing.
3291 }
3292
Marc Blank0bbc8582012-04-23 15:07:57 -07003293 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003294 public void onTextChanged(CharSequence s, int start, int before, int count) {
3295 // Do nothing.
3296 }
3297
3298
3299 // There is a big difference between the text associated with an address changing
3300 // to add the display name or to format properly and a recipient being added or deleted.
3301 // Make sure we only notify of changes when a recipient has been added or deleted.
3302 private class RecipientTextWatcher implements TextWatcher {
3303 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3304
3305 private RecipientEditTextView mView;
3306
3307 private TextWatcher mListener;
3308
3309 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3310 mView = view;
3311 mListener = listener;
3312 }
3313
3314 @Override
3315 public void afterTextChanged(Editable s) {
3316 if (hasChanged()) {
3317 mListener.afterTextChanged(s);
3318 }
3319 }
3320
3321 private boolean hasChanged() {
3322 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
3323 int totalCount = currRecips.length;
3324 int totalPrevCount = 0;
3325 for (Entry<String, Integer> entry : mContent.entrySet()) {
3326 totalPrevCount += entry.getValue();
3327 }
3328 if (totalCount != totalPrevCount) {
3329 return true;
3330 }
3331
3332 for (String recip : currRecips) {
3333 if (!mContent.containsKey(recip)) {
3334 return true;
3335 } else {
3336 int count = mContent.get(recip) - 1;
3337 if (count < 0) {
3338 return true;
3339 } else {
3340 mContent.put(recip, count);
3341 }
3342 }
3343 }
3344 return false;
3345 }
3346
3347 private String[] tokenizeRecips(String[] recips) {
3348 // Tokenize them all and put them in the list.
3349 String[] recipAddresses = new String[recips.length];
3350 for (int i = 0; i < recips.length; i++) {
3351 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
3352 }
3353 return recipAddresses;
3354 }
3355
3356 @Override
3357 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3358 String[] recips = tokenizeRecips(getAddressesFromList(mView));
3359 for (String recip : recips) {
3360 if (!mContent.containsKey(recip)) {
3361 mContent.put(recip, 1);
3362 } else {
3363 mContent.put(recip, (mContent.get(recip)) + 1);
3364 }
3365 }
3366 }
3367
3368 @Override
3369 public void onTextChanged(CharSequence s, int start, int before, int count) {
3370 // Do nothing.
3371 }
3372 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003373
3374 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3375 if (sTestSendOrSaveCallback != null && testCallback != null) {
3376 throw new IllegalStateException("Attempting to register more than one test callback");
3377 }
3378 sTestSendOrSaveCallback = testCallback;
3379 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003380
3381 @VisibleForTesting
3382 protected ArrayList<Attachment> getAttachments() {
3383 return mAttachmentsView.getAttachments();
3384 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003385
3386 @Override
3387 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3388 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003389 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3390 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3391 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003392 case REFERENCE_MESSAGE_LOADER:
3393 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3394 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003395 case LOADER_ACCOUNT_CURSOR:
3396 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3397 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003398 }
3399 return null;
3400 }
3401
3402 @Override
3403 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003404 int id = loader.getId();
3405 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003406 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003407 if (data != null && data.moveToFirst()) {
3408 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003409 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003410 initFromRefMessage(mComposeMode);
3411 finishSetup(mComposeMode, intent, null);
3412 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003413 String to = intent.getStringExtra(EXTRA_TO);
3414 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003415 mRefMessage.setTo(null);
3416 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003417 clearChangeListeners();
3418 mTo.append(to);
3419 initChangeListeners();
3420 }
3421 }
3422 } else {
3423 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003424 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003425 break;
Alice Yanga990a712013-03-13 18:37:00 -07003426 case REFERENCE_MESSAGE_LOADER:
3427 // Only populate mRefMessage and leave other fields untouched.
3428 if (data != null && data.moveToFirst()) {
3429 mRefMessage = new Message(data);
3430 }
Andy Huang9f855d62013-05-30 17:15:03 -07003431 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003432 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003433 case LOADER_ACCOUNT_CURSOR:
3434 if (data != null && data.moveToFirst()) {
3435 // there are accounts now!
3436 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003437 final ArrayList<Account> accounts = new ArrayList<Account>();
3438 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003439 do {
3440 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003441 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003442 initializedAccounts.add(account);
3443 }
3444 accounts.add(account);
3445 } while (data.moveToNext());
3446 if (initializedAccounts.size() > 0) {
3447 findViewById(R.id.wait).setVisibility(View.GONE);
3448 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3449 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003450 mAccounts = initializedAccounts.toArray(
3451 new Account[initializedAccounts.size()]);
3452
Mindy Pereirab199d172012-08-13 11:04:03 -07003453 finishCreate();
3454 invalidateOptionsMenu();
3455 } else {
3456 // Show "waiting"
3457 account = accounts.size() > 0 ? accounts.get(0) : null;
3458 showWaitFragment(account);
3459 }
3460 }
3461 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003462 }
3463 }
3464
Mindy Pereirab199d172012-08-13 11:04:03 -07003465 private void showWaitFragment(Account account) {
3466 WaitFragment fragment = getWaitFragment();
3467 if (fragment != null) {
3468 fragment.updateAccount(account);
3469 } else {
3470 findViewById(R.id.wait).setVisibility(View.VISIBLE);
3471 replaceFragment(WaitFragment.newInstance(account, true),
3472 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3473 }
3474 }
3475
3476 private WaitFragment getWaitFragment() {
3477 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3478 }
3479
3480 private int replaceFragment(Fragment fragment, int transition, String tag) {
3481 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003482 fragmentTransaction.setTransition(transition);
3483 fragmentTransaction.replace(R.id.wait, fragment, tag);
3484 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3485 return transactionId;
3486 }
3487
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003488 @Override
3489 public void onLoaderReset(Loader<Cursor> arg0) {
3490 // Do nothing.
3491 }
Paul Westbrook83e6b572013-02-05 16:22:42 -08003492
3493 @Override
3494 public Context getActivityContext() {
3495 return this;
3496 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003497}