blob: d3a552bab967ccd8d30461d1409c3ec7e50e5c5a [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;
Mindy Pereira6349a042012-01-04 11:25:01 -080029import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080030import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080031import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070032import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080033import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080034import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070035import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080036import android.content.pm.ActivityInfo;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -070037import android.content.res.Resources;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080038import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080039import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080040import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080041import android.os.Handler;
42import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070043import android.os.ParcelFileDescriptor;
Alice Yang1ebc2db2013-03-14 21:21:44 -070044import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080045import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080046import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080047import android.text.Html;
mindyped9c2f02012-10-12 10:02:08 -070048import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080050import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080051import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080052import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080053import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070054import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070055import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080056import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080057import android.view.Menu;
58import android.view.MenuInflater;
59import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080060import android.view.View;
61import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070062import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080063import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070064import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080065import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080066import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070067import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080068import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080069import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080070
Mindy Pereirac17d0732011-12-29 10:46:19 -080071import com.android.common.Rfc822Validator;
Tony Mantler9f324232013-08-08 14:24:30 -070072import com.android.common.contacts.DataUsageStatUpdater;
Andy Huang5c5fd572012-04-08 18:19:29 -070073import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080074import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070075import com.android.mail.R;
Andy Huang761522c2013-08-08 13:09:11 -070076import com.android.mail.analytics.Analytics;
Alice Yang1ebc2db2013-03-14 21:21:44 -070077import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070078import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080079import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080080import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080081import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080082import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080083import com.android.mail.providers.Address;
84import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080085import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070086import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080087import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080088import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070089import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080090import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080091import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070092import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070093import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -070094import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Paul Westbrook83e6b572013-02-05 16:22:42 -080095import com.android.mail.ui.FeedbackEnabledActivity;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -070096import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -070097import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -070098import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -070099import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -0800100import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -0700101import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -0800102import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -0800103import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800104import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800105import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800106import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800107
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700108import java.io.FileNotFoundException;
109import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700110import java.io.UnsupportedEncodingException;
111import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800112import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700113import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800114import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800115import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800116import java.util.HashSet;
117import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700118import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700119import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800120import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800121
122public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Tony Mantler2558b502013-07-09 10:53:34 -0700123 RespondInlineListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700124 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
125 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
126 FeedbackEnabledActivity {
Mindy Pereira6349a042012-01-04 11:25:01 -0800127 // Identifiers for which type of composition this is
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700128 public static final int COMPOSE = -1;
129 public static final int REPLY = 0;
130 public static final int REPLY_ALL = 1;
131 public static final int FORWARD = 2;
132 public static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800133
134 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700135 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800136
Mindy Pereira326689d2012-05-17 10:14:14 -0700137 private static final String EXTRA_SHOW_CC = "showCc";
138 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800139 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800140 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700141
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700142 private static final String UTF8_ENCODING_NAME = "UTF-8";
143
144 private static final String MAIL_TO = "mailto";
145
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700146 private static final String EXTRA_SUBJECT = "subject";
147
148 private static final String EXTRA_BODY = "body";
149
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700150 /**
151 * Expected to be html formatted text.
152 */
153 private static final String EXTRA_QUOTED_TEXT = "quotedText";
154
mindypd27b6ea2012-10-05 09:43:49 -0700155 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700156
Mark Wei62066e42012-09-13 12:07:02 -0700157 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
158
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700159 // Extra that we can get passed from other activities
160 private static final String EXTRA_TO = "to";
161 private static final String EXTRA_CC = "cc";
162 private static final String EXTRA_BCC = "bcc";
163
164 // List of all the fields
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700165 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
166 EXTRA_QUOTED_TEXT };
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700167
Mindy Pereira82cc5662012-01-09 17:29:30 -0800168 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
169 // Map containing information about requests to create new messages, and the id of the
170 // messages that were the result of those requests.
171 //
172 // This map is used when the activity that initiated the save a of a new message, is killed
173 // before the save has completed (and when we know the id of the newly created message). When
174 // a save is completed, the service that is running in the background, will update the map
175 //
176 // When a new ComposeActivity instance is created, it will attempt to use the information in
177 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
178 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
179 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700180 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800181 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800182 /**
183 * Notifies the {@code Activity} that the caller is an Email
184 * {@code Activity}, so that the back behavior may be modified accordingly.
185 *
186 * @see #onAppUpPressed
187 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700188 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800189
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700190 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700191
Scott Kennedy5680ec22013-01-07 13:15:20 -0800192 /** If set, we will clear notifications for this folder. */
193 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
194
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800195 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700196 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700197 // If this is a reply/forward then this extra will hold a uri we must query
198 // to get the original message.
199 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700200 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700201 // draft message
202 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800203 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700204 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800205 // Request numbers for activities we start
206 private static final int RESULT_PICK_ATTACHMENT = 1;
207 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700208 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700209 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700210
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700211 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
212 private static final String EXTRA_REQUEST_ID = "requestId";
213 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
Paul Westbrook176a1992013-07-22 13:57:19 -0700214 private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700215 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700216 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700217 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700218 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700219 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700220 private static final String TAG_WAIT = "wait-fragment";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700221 private static final String MIME_TYPE_PHOTO = "image/*";
222 private static final String MIME_TYPE_VIDEO = "video/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800223
Andy Huang9f855d62013-05-30 17:15:03 -0700224 private static final String KEY_INNER_SAVED_STATE = "compose_state";
225
Mindy Pereira82cc5662012-01-09 17:29:30 -0800226 /**
227 * A single thread for running tasks in the background.
228 */
229 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800230 private RecipientEditTextView mTo;
231 private RecipientEditTextView mCc;
232 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800233 private Button mCcBccButton;
234 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800235 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800236 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700237 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800238 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800239 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800240 private TextView mSubject;
241
Mindy Pereira326c6602012-01-04 15:32:42 -0800242 private ComposeModeAdapter mComposeModeAdapter;
243 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800244 private boolean mForward;
245 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800246 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700247 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800248 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800249 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800250 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700251 @VisibleForTesting
252 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800253 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800254 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800255 private boolean mTextChanged;
256 private boolean mReplyFromChanged;
257 private MenuItem mSave;
258 private MenuItem mSend;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700259 @VisibleForTesting
260 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800261 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
262 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800263 private ReplyFromAccount mDraftAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800264 private Object mDraftLock = new Object();
mindyp93b079b2012-08-29 16:32:15 -0700265 private View mPhotoAttachmentsButton;
266 private View mVideoAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800267
Mindy Pereira326c6602012-01-04 15:32:42 -0800268 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700269 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
270 */
271 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700272 private RecipientTextWatcher mToListener;
273 private RecipientTextWatcher mCcListener;
274 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700275 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700276 private boolean mShowQuotedText = false;
Andy Huang9f855d62013-05-30 17:15:03 -0700277 private Bundle mInnerSavedState;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700278
279
mindyp1623f9b2012-11-21 12:41:16 -0800280 // Array of the outstanding send or save tasks. Access is synchronized
281 // with the object itself
282 /* package for testing */
283 @VisibleForTesting
284 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
285 // FIXME: this variable is never read. related to sRequestMessageIdMap.
286 private int mRequestId;
287 private String mSignature;
288 private Account[] mAccounts;
289 private boolean mRespondedInline;
290
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700291 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800292 * Can be called from a non-UI thread.
293 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800294 public static void editDraft(Context launcher, Account account, Message message) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700295 launch(launcher, account, message, EDIT_DRAFT, null, null, null, null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800296 }
297
Mindy Pereira6349a042012-01-04 11:25:01 -0800298 /**
299 * Can be called from a non-UI thread.
300 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800301 public static void compose(Context launcher, Account account) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700302 launch(launcher, account, null, COMPOSE, null, null, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800303 }
304
305 /**
306 * Can be called from a non-UI thread.
307 */
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700308 public static void composeToAddress(Context launcher, Account account, String toAddress) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700309 launch(launcher, account, null, COMPOSE, toAddress, null, null, null);
310 }
311
312 /**
313 * Can be called from a non-UI thread.
314 */
315 public static void composeWithQuotedText(Context launcher, Account account,
316 String quotedText, String subject) {
317 launch(launcher, account, null, COMPOSE, null, null, quotedText, subject);
Andrew Sapperstein3de76ec2013-07-16 12:08:15 -0700318 }
319
320 /**
321 * Can be called from a non-UI thread.
322 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800323 public static Intent createReplyIntent(final Context launcher, final Account account,
324 final Uri messageUri, final boolean isReplyAll) {
325 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
326 }
327
328 /**
329 * Can be called from a non-UI thread.
330 */
331 public static Intent createForwardIntent(final Context launcher, final Account account,
332 final Uri messageUri) {
333 return createActionIntent(launcher, account, messageUri, FORWARD);
334 }
335
336 private static Intent createActionIntent(final Context launcher, final Account account,
337 final Uri messageUri, final int action) {
338 final Intent intent = new Intent(launcher, ComposeActivity.class);
339
Paul Westbrook6d2442b2013-07-17 17:51:51 -0700340 updateActionIntent(account, messageUri, action, intent);
341
342 return intent;
343 }
344
345 @VisibleForTesting
346 static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800347 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
348 intent.putExtra(EXTRA_ACTION, action);
349 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
350 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
351
352 return intent;
353 }
354
355 /**
356 * Can be called from a non-UI thread.
357 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800358 public static void reply(Context launcher, Account account, Message message) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700359 launch(launcher, account, message, REPLY, null, null, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800360 }
361
362 /**
363 * Can be called from a non-UI thread.
364 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800365 public static void replyAll(Context launcher, Account account, Message message) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700366 launch(launcher, account, message, REPLY_ALL, null, null, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800367 }
368
369 /**
370 * Can be called from a non-UI thread.
371 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800372 public static void forward(Context launcher, Account account, Message message) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700373 launch(launcher, account, message, FORWARD, null, null, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800374 }
375
Alice Yang1ebc2db2013-03-14 21:21:44 -0700376 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
377 String body) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700378 launch(launcher, account, message, FORWARD,
379 "android-gmail-readability@google.com", body, null, null);
Alice Yang1ebc2db2013-03-14 21:21:44 -0700380 }
381
382 private static void launch(Context launcher, Account account, Message message, int action,
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700383 String toAddress, String body, String quotedText, String subject) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800384 Intent intent = new Intent(launcher, ComposeActivity.class);
385 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
386 intent.putExtra(EXTRA_ACTION, action);
387 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700388 if (action == EDIT_DRAFT) {
389 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
390 } else {
391 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
392 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700393 if (toAddress != null) {
394 intent.putExtra(EXTRA_TO, toAddress);
395 }
396 if (body != null) {
397 intent.putExtra(EXTRA_BODY, body);
398 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700399 if (quotedText != null) {
400 intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
401 }
402 if (subject != null) {
403 intent.putExtra(EXTRA_SUBJECT, subject);
404 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800405 launcher.startActivity(intent);
406 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800407
408 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700409 protected void onCreate(Bundle savedInstanceState) {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800410 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800411 setContentView(R.layout.compose);
Andy Huang9f855d62013-05-30 17:15:03 -0700412 mInnerSavedState = (savedInstanceState != null) ?
413 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700414 checkValidAccounts();
415 }
416
417 private void finishCreate() {
Andy Huang9f855d62013-05-30 17:15:03 -0700418 final Bundle savedState = mInnerSavedState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800419 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800420 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700421 Message message;
Mark Wei62066e42012-09-13 12:07:02 -0700422 ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700423 mShowQuotedText = false;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700424 CharSequence quotedText = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700425 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700426 // Check for any of the possibly supplied accounts.;
427 Account account = null;
Andy Huang9f855d62013-05-30 17:15:03 -0700428 if (hadSavedInstanceStateMessage(savedState)) {
429 action = savedState.getInt(EXTRA_ACTION, COMPOSE);
430 account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
431 message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700432
Andy Huang9f855d62013-05-30 17:15:03 -0700433 previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
434 mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700435 quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700436 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700437 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700438 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
439 // Initialize the message from the message in the intent
440 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700441 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700442 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700443 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700444 }
Mark Wei62066e42012-09-13 12:07:02 -0700445 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700446
447 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800448 if (mAccount == null) {
449 return;
450 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700451
Scott Kennedyfe853d32013-06-19 11:47:35 -0700452 initRecipients();
453
Scott Kennedy5680ec22013-01-07 13:15:20 -0800454 // Clear the notification and mark the conversation as seen, if necessary
455 final Folder notificationFolder =
456 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
457 if (notificationFolder != null) {
458 final Intent clearNotifIntent =
459 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800460 clearNotifIntent.setPackage(getPackageName());
Scott Kennedy48cfe462013-04-10 11:32:02 -0700461 clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
462 clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800463
464 startService(clearNotifIntent);
465 }
466
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700467 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
468 mLaunchedFromEmail = true;
469 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
470 final Uri dataUri = intent.getData();
471 if (dataUri != null) {
472 final String dataScheme = intent.getData().getScheme();
473 final String accountScheme = mAccount.composeIntentUri.getScheme();
474 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
475 }
476 }
477
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700478 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700479 mShowQuotedText = true;
480 mComposeMode = action;
481 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700482 return;
483 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700484 initFromDraftMessage(message);
485 initQuotedTextFromRefMessage(mRefMessage, action);
Andy Huang9f855d62013-05-30 17:15:03 -0700486 showCcBcc(savedState);
Alice Yanga990a712013-03-13 18:37:00 -0700487 mShowQuotedText = message.appendRefMessageContent;
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700488 // if we should be showing quoted text but mRefMessage is null
489 // and we have some quotedText, display that
490 if (mShowQuotedText && quotedText != null && mRefMessage == null) {
491 initQuotedText(quotedText, false /* shouldQuoteText */);
492 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700493 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700494 initFromDraftMessage(message);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800495 boolean showBcc = !TextUtils.isEmpty(message.getBcc());
496 boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
Mindy Pereiraef388302012-06-18 19:07:44 -0700497 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700498 // Update the action to the draft type of the previous draft
499 switch (message.draftType) {
500 case UIProvider.DraftType.REPLY:
501 action = REPLY;
502 break;
503 case UIProvider.DraftType.REPLY_ALL:
504 action = REPLY_ALL;
505 break;
506 case UIProvider.DraftType.FORWARD:
507 action = FORWARD;
508 break;
509 case UIProvider.DraftType.COMPOSE:
510 default:
511 action = COMPOSE;
512 break;
513 }
Alice Yanga990a712013-03-13 18:37:00 -0700514 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
515
516 mShowQuotedText = message.appendRefMessageContent;
517 if (message.refMessageUri != null) {
518 // If we're editing an existing draft that was in reference to an existing message,
519 // still need to load that original message since we might need to refer to the
520 // original sender and recipients if user switches "reply <-> reply-all".
521 mRefMessageUri = message.refMessageUri;
522 mComposeMode = action;
523 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
524 return;
525 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700526 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
527 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800528 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700529 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700530 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700531 } else {
532 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700533 }
Alice Yanga990a712013-03-13 18:37:00 -0700534
535 mComposeMode = action;
Andy Huang9f855d62013-05-30 17:15:03 -0700536 finishSetup(action, intent, savedState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700537 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700538
Mindy Pereirab199d172012-08-13 11:04:03 -0700539 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700540 final Account[] allAccounts = AccountUtils.getAccounts(this);
541 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700542 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
543 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700544 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700545 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
546 }
547 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700548 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700549 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700550 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700551 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700552 anySyncing = true;
553 break;
554 }
555 }
556 if (!anySyncing) {
557 // There are accounts, but none are sync'd, which is just like having no accounts.
558 mAccounts = null;
559 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
560 return;
561 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700562 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700563 finishCreate();
564 }
565 }
566
Mindy Pereira47d0e652012-07-23 09:45:07 -0700567 private Account obtainAccount(Intent intent) {
568 Account account = null;
569 Object accountExtra = null;
570 if (intent != null && intent.getExtras() != null) {
571 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
572 if (accountExtra instanceof Account) {
573 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700574 } else if (accountExtra instanceof String) {
575 // This is the Account attached to the widget compose intent.
576 account = Account.newinstance((String)accountExtra);
577 if (account != null) {
578 return account;
579 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700580 }
mindyp5ee9dc42013-01-08 09:54:54 -0800581 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
582 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
583 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700584 }
585 if (account == null) {
mindyp06174462012-10-12 09:11:27 -0700586 MailAppProvider provider = MailAppProvider.getInstance();
587 String lastAccountUri = provider.getLastSentFromAccount();
588 if (TextUtils.isEmpty(lastAccountUri)) {
589 lastAccountUri = provider.getLastViewedAccount();
590 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700591 if (!TextUtils.isEmpty(lastAccountUri)) {
592 accountExtra = Uri.parse(lastAccountUri);
593 }
594 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700595 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700596 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
597 // For backwards compatibility, we need to check account
598 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700599 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700600 if (a.name.equals(accountExtra)) {
601 account = a;
602 }
603 }
604 } else if (accountExtra instanceof Uri) {
605 // The uri of the last viewed account is what is stored in
606 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700607 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700608 if (a.uri.equals(accountExtra)) {
609 account = a;
610 }
611 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700612 }
613 if (account == null) {
614 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700615 }
616 }
617 return account;
618 }
619
Alice Yanga990a712013-03-13 18:37:00 -0700620 private void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800621 setFocus(action);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700622 // Don't bother with the intent if we have procured a message from the
623 // intent already.
624 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
625 initAttachmentsFromIntent(intent);
626 }
Alice Yanga990a712013-03-13 18:37:00 -0700627 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700628 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
629 action);
mindypd4a48662012-11-08 17:13:49 -0800630
631 // If this is a draft message, the draft account is whatever account was
632 // used to open the draft message in Compose.
633 if (mDraft != null) {
634 mDraftAccount = mReplyFromAccount;
635 }
636
Mindy Pereira75f66632012-01-11 11:42:02 -0800637 initChangeListeners();
Mindy Pereira326689d2012-05-17 10:14:14 -0700638 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700639 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800640
Andy Huang9f855d62013-05-30 17:15:03 -0700641 mRespondedInline = mInnerSavedState != null ?
642 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
mindyp1623f9b2012-11-21 12:41:16 -0800643 if (mRespondedInline) {
644 mQuotedTextView.setVisibility(View.GONE);
645 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700646 }
647
Scott Kennedyff8553f2013-04-05 20:57:44 -0700648 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700649 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
650 }
651
Mindy Pereira71c9e562012-05-17 11:01:02 -0700652 private void updateHideOrShowQuotedText(boolean showQuotedText) {
653 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700654 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700655 }
656
657 private void setFocus(int action) {
658 if (action == EDIT_DRAFT) {
659 int type = mDraft.draftType;
660 switch (type) {
661 case UIProvider.DraftType.COMPOSE:
662 case UIProvider.DraftType.FORWARD:
663 action = COMPOSE;
664 break;
665 case UIProvider.DraftType.REPLY:
666 case UIProvider.DraftType.REPLY_ALL:
667 default:
668 action = REPLY;
669 break;
670 }
671 }
672 switch (action) {
673 case FORWARD:
674 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800675 if (TextUtils.isEmpty(mTo.getText())) {
676 mTo.requestFocus();
677 break;
678 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700679 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700680 case REPLY:
681 case REPLY_ALL:
682 default:
683 focusBody();
684 break;
685 }
686 }
687
688 /**
689 * Focus the body of the message.
690 */
691 public void focusBody() {
692 mBodyView.requestFocus();
693 int length = mBodyView.getText().length();
694
695 int signatureStartPos = getSignatureStartPosition(
696 mSignature, mBodyView.getText().toString());
697 if (signatureStartPos > -1) {
698 // In case the user deleted the newlines...
699 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700700 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700701 // Move cursor to the end.
702 mBodyView.setSelection(length);
703 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800704 }
705
706 @Override
Andy Huang761522c2013-08-08 13:09:11 -0700707 protected void onStart() {
708 super.onStart();
709
710 Analytics.getInstance().activityStart(this);
711 }
712
713 @Override
714 protected void onStop() {
715 super.onStop();
716
717 Analytics.getInstance().activityStop(this);
718 }
719
720 @Override
Mindy Pereira1a95a572012-01-05 12:21:29 -0800721 protected void onResume() {
722 super.onResume();
723 // Update the from spinner as other accounts
724 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800725 if (mFromSpinner != null && mAccount != null) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700726 mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
Mindy Pereira818143e2012-01-11 13:59:49 -0800727 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800728 }
729
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800730 @Override
731 protected void onPause() {
732 super.onPause();
733
Mindy Pereiraa2148332012-07-02 13:54:14 -0700734 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700735 // Don't save unnecessary drafts if we are only changing the orientation.
736 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700737 saveIfNeeded();
738 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800739 }
740
741 @Override
742 protected final void onActivityResult(int request, int result, Intent data) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700743 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800744 addAttachmentAndUpdateView(data);
Mindy Pereirab199d172012-08-13 11:04:03 -0700745 mAddingAttachment = false;
746 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700747 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700748 if (result != RESULT_OK) {
749 finish();
750 } else {
751 // Watch for accounts to show up!
752 // restart the loader to get the updated list of accounts
753 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
754 showWaitFragment(null);
755 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800756 }
757 }
758
759 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700760 protected final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700761 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
762 if (hasAccounts) {
763 clearChangeListeners();
764 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700765 super.onRestoreInstanceState(savedInstanceState);
Andy Huang9f855d62013-05-30 17:15:03 -0700766 if (mInnerSavedState != null) {
767 if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
768 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
769 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700770 // There should be a focus and it should be an EditText since we
771 // only save these extras if these conditions are true.
772 EditText focusEditText = (EditText) getCurrentFocus();
773 final int length = focusEditText.getText().length();
774 if (selectionStart < length && selectionEnd < length) {
775 focusEditText.setSelection(selectionStart, selectionEnd);
776 }
777 }
778 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700779 if (hasAccounts) {
780 initChangeListeners();
781 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700782 }
783
784 @Override
Scott Kennedyd9063902013-08-02 22:14:37 -0700785 protected final void onSaveInstanceState(Bundle state) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800786 super.onSaveInstanceState(state);
Andy Huang9f855d62013-05-30 17:15:03 -0700787 final Bundle inner = new Bundle();
788 saveState(inner);
789 state.putBundle(KEY_INNER_SAVED_STATE, inner);
790 }
791
792 private void saveState(Bundle state) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700793 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
794 if (mAccounts == null || mAccounts.length == 0) {
795 return;
796 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700797 // The framework is happy to save and restore the selection but only if it also saves and
798 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
799 // this manually.
800 View focus = getCurrentFocus();
801 if (focus != null && focus instanceof EditText) {
802 EditText focusEditText = (EditText) focus;
803 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
804 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
805 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700806
807 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700808 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700809 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
810 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
811 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700812 if (selectedReplyFromAccount != null) {
813 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
814 .toString());
815 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
816 } else {
817 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
818 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800819
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700820 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
821 // We don't have a draft id, and we have a request id,
822 // save the request id.
823 state.putInt(EXTRA_REQUEST_ID, mRequestId);
824 }
825
826 // We want to restore the current mode after a pause
827 // or rotation.
828 int mode = getMode();
829 state.putInt(EXTRA_ACTION, mode);
830
mindype7b76aa2012-11-14 16:19:13 -0800831 final Message message = createMessage(selectedReplyFromAccount, mode);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700832 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -0800833 message.id = mDraft.id;
834 message.serverId = mDraft.serverId;
835 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700836 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700837 state.putParcelable(EXTRA_MESSAGE, message);
838
839 if (mRefMessage != null) {
840 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -0700841 } else if (message.appendRefMessageContent) {
842 // If we have no ref message but should be appending
843 // ref message content, we have orphaned quoted text. Save it.
844 state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700845 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700846 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
847 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -0800848 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -0800849 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -0700850 state.putParcelableArrayList(
851 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700852 }
853
854 private int getMode() {
855 int mode = ComposeActivity.COMPOSE;
856 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700857 if (actionBar != null
858 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700859 mode = actionBar.getSelectedNavigationIndex();
860 }
861 return mode;
862 }
863
864 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
865 Message message = new Message();
866 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -0700867 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700868 message.uri = null;
869 message.conversationUri = null;
870 message.subject = mSubject.getText().toString();
871 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800872 message.setTo(formatSenders(mTo.getText().toString()));
873 message.setCc(formatSenders(mCc.getText().toString()));
874 message.setBcc(formatSenders(mBcc.getText().toString()));
875 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700876 message.dateReceivedMs = 0;
Paul Westbrookb4931c62013-01-14 17:51:18 -0800877 final String htmlBody = Html.toHtml(removeComposingSpans(mBodyView.getText()));
878 final StringBuilder fullBody = new StringBuilder(htmlBody);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700879 message.bodyHtml = fullBody.toString();
880 message.bodyText = mBodyView.getText().toString();
881 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -0700882 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700883 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
884 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
885 message.hasAttachments = attachments != null && attachments.size() > 0;
886 message.attachmentListUri = null;
887 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700888 message.alwaysShowImages = false;
889 message.attachmentsJson = Attachment.toJSONArray(attachments);
890 CharSequence quotedText = mQuotedTextView.getQuotedText();
891 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
892 .getQuotedTextOffset(quotedText.toString()) : -1;
893 message.accountUri = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800894 message.setFrom(selectedReplyFromAccount != null ? selectedReplyFromAccount.address
895 : mAccount != null ? mAccount.name : null);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700896 message.draftType = getDraftType(mode);
mindype7b76aa2012-11-14 16:19:13 -0800897 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700898 }
899
Scott Kennedyff8553f2013-04-05 20:57:44 -0700900 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -0700901 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
902 return string.substring(0, string.length() - 1);
903 }
904 return string;
905 }
906
Mindy Pereira818143e2012-01-11 13:59:49 -0800907 @VisibleForTesting
908 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700909 if (account == null) {
910 return;
911 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700912 if (!account.equals(mAccount)) {
913 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700914 mCachedSettings = mAccount.settings;
915 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700916 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700917 if (mAccount != null) {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800918 MailActivity.setNfcMessage(mAccount.name);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700919 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800920 }
921
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700922 private void initFromSpinner(Bundle bundle, int action) {
923 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700924 action = COMPOSE;
925 }
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700926 mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
927
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700928 if (bundle != null) {
929 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
930 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
931 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
932 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700933 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700934 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
935 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700936 }
937 if (mReplyFromAccount == null) {
938 if (mDraft != null) {
939 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
940 } else if (mRefMessage != null) {
941 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
942 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700943 }
944 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -0700945 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700946 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700947
Mindy Pereira62de1b12012-04-06 12:17:56 -0700948 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700949
Mindy Pereira62de1b12012-04-06 12:17:56 -0700950 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700951 // If there is only 1 account, just show that account.
952 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700953 // send mail from / save drafts to.
954 mFromStatic.setVisibility(View.GONE);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700955 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700956 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700957 } else {
958 mFromStatic.setVisibility(View.VISIBLE);
Paul Westbrookc97ec3e2013-07-12 18:17:19 -0700959 mFromStaticText.setText(mReplyFromAccount.name);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700960 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700961 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800962 }
963
Mindy Pereira62de1b12012-04-06 12:17:56 -0700964 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
965 if (refMessage.accountUri != null) {
966 // This must be from combined inbox.
967 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
968 for (ReplyFromAccount from : replyFromAccounts) {
969 if (from.account.uri.equals(refMessage.accountUri)) {
970 return from;
971 }
972 }
973 return null;
974 } else {
975 return getReplyFromAccount(account, refMessage);
976 }
977 }
978
979 /**
Tony Mantler9016a5e2013-07-19 11:54:17 -0700980 * Given an account and the message we're replying to,
Mindy Pereira62de1b12012-04-06 12:17:56 -0700981 * return who the message should be sent from.
982 * @param account Account in which the message arrived.
Tony Mantler9016a5e2013-07-19 11:54:17 -0700983 * @param refMessage Message to analyze for account selection
Mindy Pereira62de1b12012-04-06 12:17:56 -0700984 * @return the address from which to reply.
985 */
986 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
987 // First see if we are supposed to use the default address or
988 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -0700989 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700990 return getDefaultReplyFromAccount(account);
991 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -0700992 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -0700993 // all the message recipients and find one that matches
994 // a custom from or account.
995 List<String> allRecipients = new ArrayList<String>();
Tony Mantler9016a5e2013-07-19 11:54:17 -0700996 allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
997 allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
Mindy Pereira62de1b12012-04-06 12:17:56 -0700998 return getMatchingRecipient(account, allRecipients);
999 }
1000 }
1001
1002 /**
1003 * Compare all the recipients of an email to the current account and all
1004 * custom addresses associated with that account. Return the match if there
1005 * is one, or the default account if there isn't.
1006 */
1007 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
1008 // Tokenize the list and place in a hashmap.
1009 ReplyFromAccount matchingReplyFrom = null;
1010 Rfc822Token[] tokens;
1011 HashSet<String> recipientsMap = new HashSet<String>();
1012 for (String address : sentTo) {
1013 tokens = Rfc822Tokenizer.tokenize(address);
1014 for (int i = 0; i < tokens.length; i++) {
1015 recipientsMap.add(tokens[i].getAddress());
1016 }
1017 }
1018
1019 int matchingAddressCount = 0;
1020 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -07001021 customFroms = account.getReplyFroms();
1022 if (customFroms != null) {
1023 for (ReplyFromAccount entry : customFroms) {
1024 if (recipientsMap.contains(entry.address)) {
1025 matchingReplyFrom = entry;
1026 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -07001027 }
1028 }
Mindy Pereira62de1b12012-04-06 12:17:56 -07001029 }
1030 if (matchingAddressCount > 1) {
1031 matchingReplyFrom = getDefaultReplyFromAccount(account);
1032 }
1033 return matchingReplyFrom;
1034 }
1035
Scott Kennedyff8553f2013-04-05 20:57:44 -07001036 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
1037 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -07001038 if (from.isDefault) {
1039 return from;
1040 }
1041 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -07001042 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
1043 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001044 }
1045
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001046 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001047 String sender = msg.getFrom();
Mindy Pereira62de1b12012-04-06 12:17:56 -07001048 ReplyFromAccount replyFromAccount = null;
1049 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
1050 if (TextUtils.equals(account.name, sender)) {
1051 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -07001052 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -07001053 } else {
1054 for (ReplyFromAccount fromAccount : replyFromAccounts) {
1055 if (TextUtils.equals(fromAccount.name, sender)) {
1056 replyFromAccount = fromAccount;
1057 break;
1058 }
1059 }
1060 }
1061 return replyFromAccount;
1062 }
1063
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001064 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001065 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001066 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001067 if (mCcBccButton != null) {
1068 mCcBccButton.setOnClickListener(this);
1069 }
1070 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001071 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
mindyp93b079b2012-08-29 16:32:15 -07001072 mPhotoAttachmentsButton = findViewById(R.id.add_photo_attachment);
mindypcd0b0b92012-08-23 14:33:17 -07001073 if (mPhotoAttachmentsButton != null) {
1074 mPhotoAttachmentsButton.setOnClickListener(this);
1075 }
mindyp93b079b2012-08-29 16:32:15 -07001076 mVideoAttachmentsButton = findViewById(R.id.add_video_attachment);
mindypcd0b0b92012-08-23 14:33:17 -07001077 if (mVideoAttachmentsButton != null) {
1078 mVideoAttachmentsButton.setOnClickListener(this);
1079 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001080 mTo = (RecipientEditTextView) findViewById(R.id.to);
Scott Kennedy41500392013-04-24 18:46:36 -07001081 mTo.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001082 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Scott Kennedy41500392013-04-24 18:46:36 -07001083 mCc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001084 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Scott Kennedy41500392013-04-24 18:46:36 -07001085 mBcc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001086 // TODO: add special chips text change watchers before adding
1087 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001088 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001089 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001090 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1091 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001092 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001093 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001094 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001095 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001096 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001097 }
1098
mindyp62d3ec72012-08-24 13:04:09 -07001099 @Override
1100 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1101 if (action == EditorInfo.IME_ACTION_DONE) {
1102 focusBody();
1103 return true;
1104 }
1105 return false;
1106 }
1107
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001108 protected TextView getBody() {
1109 return mBodyView;
1110 }
1111
1112 @VisibleForTesting
1113 public Account getFromAccount() {
1114 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1115 mReplyFromAccount.account : mAccount;
1116 }
1117
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001118 private void clearChangeListeners() {
1119 mSubject.removeTextChangedListener(this);
1120 mBodyView.removeTextChangedListener(this);
1121 mTo.removeTextChangedListener(mToListener);
1122 mCc.removeTextChangedListener(mCcListener);
1123 mBcc.removeTextChangedListener(mBccListener);
1124 mFromSpinner.setOnAccountChangedListener(null);
1125 mAttachmentsView.setAttachmentChangesListener(null);
1126 }
1127
Mindy Pereira75f66632012-01-11 11:42:02 -08001128 // Now that the message has been initialized from any existing draft or
1129 // ref message data, set up listeners for any changes that occur to the
1130 // message.
1131 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001132 // Make sure we only add text changed listeners once!
1133 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001134 mSubject.addTextChangedListener(this);
1135 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001136 if (mToListener == null) {
1137 mToListener = new RecipientTextWatcher(mTo, this);
1138 }
1139 mTo.addTextChangedListener(mToListener);
1140 if (mCcListener == null) {
1141 mCcListener = new RecipientTextWatcher(mCc, this);
1142 }
1143 mCc.addTextChangedListener(mCcListener);
1144 if (mBccListener == null) {
1145 mBccListener = new RecipientTextWatcher(mBcc, this);
1146 }
1147 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001148 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001149 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001150 }
1151
Alice Yanga990a712013-03-13 18:37:00 -07001152 private void initActionBar() {
1153 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001154 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001155 if (actionBar == null) {
1156 return;
1157 }
Alice Yanga990a712013-03-13 18:37:00 -07001158 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001159 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1160 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001161 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001162 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001163 if (mComposeModeAdapter == null) {
1164 mComposeModeAdapter = new ComposeModeAdapter(this);
1165 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001166 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1167 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001168 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001169 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001170 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001171 break;
1172 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001173 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001174 break;
1175 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001176 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001177 break;
1178 }
1179 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001180 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1181 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1182 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001183 }
1184
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001185 private void initFromRefMessage(int action) {
1186 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001187
1188 // Check if To: address and email body needs to be prefilled based on extras.
1189 // This is used for reporting rendering feedback.
1190 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1191 Intent intent = getIntent();
1192 if (intent.getExtras() != null) {
1193 String toAddresses = intent.getStringExtra(EXTRA_TO);
1194 if (toAddresses != null) {
1195 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1196 }
1197 String body = intent.getStringExtra(EXTRA_BODY);
1198 if (body != null) {
1199 setBody(body, false /* withSignature */);
1200 }
1201 }
1202 }
1203
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001204 if (mRefMessage != null) {
1205 // CC field only gets populated when doing REPLY_ALL.
1206 // BCC never gets auto-populated, unless the user is editing
1207 // a draft with one.
Mindy Pereira29a717e2012-07-25 18:05:48 -07001208 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001209 mCcBccView.show(false, true, false);
1210 }
1211 }
1212 updateHideOrShowCcBcc();
1213 }
1214
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001215 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001216 setSubject(mRefMessage, action);
1217 // Setup recipients
1218 if (action == FORWARD) {
1219 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001220 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001221 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001222 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001223 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1224 initAttachments(mRefMessage);
1225 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001226 }
1227
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001228 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001229 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001230
1231 mDraft = message;
1232 mDraftId = message.id;
1233 mSubject.setText(message.subject);
1234 mForward = message.draftType == UIProvider.DraftType.FORWARD;
Tony Mantler9016a5e2013-07-19 11:54:17 -07001235 final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001236 addToAddresses(toAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001237 addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1238 addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001239 if (message.hasAttachments) {
1240 List<Attachment> attachments = message.getAttachments();
1241 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001242 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001243 }
1244 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -07001245 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -07001246 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001247 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001248 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001249 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001250 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001251 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001252 // Find the offset in the htmltext of the actual quoted text and strip it out.
1253 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1254 if (quotedTextIndex > -1) {
mindypc59dd822012-11-13 10:56:21 -08001255 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1256 quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001257 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1258 message.bodyHtml.length());
1259 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001260 } else {
mindypc59dd822012-11-13 10:56:21 -08001261 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001262 }
1263 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001264 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001265 final String body = message.bodyText;
1266 final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1267 (quotedTextIndex > -1 ?
1268 message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1269 : "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001270 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001271 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1272 : null;
Mindy Pereira002ff522012-05-30 10:31:26 -07001273 }
1274 mBodyView.setText(bodyText);
1275 }
1276 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001277 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001278 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001279 }
1280
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001281 /**
1282 * Fill all the widgets with the content found in the Intent Extra, if any.
1283 * Also apply the same style to all widgets. Note: if initFromExtras is
1284 * called as a result of switching between reply, reply all, and forward per
1285 * the latest revision of Gmail, and the user has already made changes to
1286 * attachments on a previous incarnation of the message (as a reply, reply
1287 * all, or forward), the original attachments from the message will not be
1288 * re-instantiated. The user's changes will be respected. This follows the
1289 * web gmail interaction.
1290 */
1291 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001292 // If we were invoked with a SENDTO intent, the value
1293 // should take precedence
1294 final Uri dataUri = intent.getData();
1295 if (dataUri != null) {
1296 if (MAIL_TO.equals(dataUri.getScheme())) {
1297 initFromMailTo(dataUri.toString());
1298 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001299 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001300 String toText = dataUri.getSchemeSpecificPart();
1301 if (toText != null) {
1302 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001303 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001304 }
1305 }
1306 }
1307 }
1308
1309 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1310 if (extraStrings != null) {
1311 addToAddresses(Arrays.asList(extraStrings));
1312 }
1313 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1314 if (extraStrings != null) {
1315 addCcAddresses(Arrays.asList(extraStrings), null);
1316 }
1317 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1318 if (extraStrings != null) {
1319 addBccAddresses(Arrays.asList(extraStrings));
1320 }
1321
1322 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1323 if (extraString != null) {
1324 mSubject.setText(extraString);
1325 }
1326
1327 for (String extra : ALL_EXTRAS) {
1328 if (intent.hasExtra(extra)) {
1329 String value = intent.getStringExtra(extra);
1330 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001331 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001332 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001333 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001334 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001335 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001336 } else if (EXTRA_SUBJECT.equals(extra)) {
1337 mSubject.setText(value);
1338 } else if (EXTRA_BODY.equals(extra)) {
1339 setBody(value, true /* with signature */);
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001340 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1341 initQuotedText(value, true /* shouldQuoteText */);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001342 }
1343 }
1344 }
1345
1346 Bundle extras = intent.getExtras();
1347 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001348 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1349 if (text != null) {
1350 setBody(text, true /* with signature */);
1351 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001352
1353 // TODO - support EXTRA_HTML_TEXT
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001354 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001355 }
1356
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001357
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001358 @VisibleForTesting
1359 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001360 // TODO: handle the case where there are spaces in the display name as
1361 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1362 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001363 // Since URLDecode.decode changes + into ' ', and + is a valid
1364 // email character, we need to find/ replace these ourselves before
1365 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001366 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001367 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001368 } catch (IllegalArgumentException e) {
1369 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1370 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1371 } else {
1372 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1373 }
1374 return null;
1375 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001376 }
1377
1378 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001379 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1380 * changing '+' into ' '
1381 *
1382 * @param toReplace Input string
1383 * @return The string with all "+" characters replaced with "%2B"
1384 */
Scott Kennedy3b965d72013-06-25 14:36:55 -07001385 private static String replacePlus(String toReplace) {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001386 return toReplace.replace("+", "%2B");
1387 }
1388
1389 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001390 * Initialize the compose view from a String representing a mailTo uri.
1391 * @param mailToString The uri as a string.
1392 */
1393 public void initFromMailTo(String mailToString) {
1394 // We need to disguise this string as a URI in order to parse it
1395 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1396 Uri uri = Uri.parse("foo://" + mailToString);
1397 int index = mailToString.indexOf("?");
1398 int length = "mailto".length() + 1;
1399 String to;
1400 try {
1401 // Extract the recipient after mailto:
1402 if (index == -1) {
1403 to = decodeEmailInUri(mailToString.substring(length));
1404 } else {
1405 to = decodeEmailInUri(mailToString.substring(length, index));
1406 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001407 if (!TextUtils.isEmpty(to)) {
1408 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1409 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001410 } catch (UnsupportedEncodingException e) {
1411 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1412 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1413 } else {
1414 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1415 }
1416 }
1417
1418 List<String> cc = uri.getQueryParameters("cc");
1419 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1420
1421 List<String> otherTo = uri.getQueryParameters("to");
1422 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1423
1424 List<String> bcc = uri.getQueryParameters("bcc");
1425 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1426
1427 List<String> subject = uri.getQueryParameters("subject");
1428 if (subject.size() > 0) {
1429 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001430 mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
1431 UTF8_ENCODING_NAME));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001432 } catch (UnsupportedEncodingException e) {
1433 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1434 e.getMessage(), subject);
1435 }
1436 }
1437
1438 List<String> body = uri.getQueryParameters("body");
1439 if (body.size() > 0) {
1440 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001441 setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001442 true /* with signature */);
1443 } catch (UnsupportedEncodingException e) {
1444 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1445 }
1446 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001447 }
1448
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001449 @VisibleForTesting
1450 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001451 addAttachments(refMessage.getAttachments());
1452 }
1453
1454 public long addAttachments(List<Attachment> attachments) {
1455 long size = 0;
1456 AttachmentFailureException error = null;
1457 for (Attachment a : attachments) {
1458 try {
1459 size += mAttachmentsView.addAttachment(mAccount, a);
1460 } catch (AttachmentFailureException e) {
1461 error = e;
1462 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001463 }
Mark Wei434f2942012-08-24 11:54:02 -07001464 if (error != null) {
1465 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1466 if (attachments.size() > 1) {
1467 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1468 } else {
1469 showAttachmentTooBigToast(error.getErrorRes());
1470 }
1471 }
1472 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001473 }
1474
1475 /**
1476 * When an attachment is too large to be added to a message, show a toast.
1477 * This method also updates the position of the toast so that it is shown
1478 * clearly above they keyboard if it happens to be open.
1479 */
Mark Wei434f2942012-08-24 11:54:02 -07001480 private void showAttachmentTooBigToast(int errorRes) {
1481 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1482 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1483 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001484 }
1485
Mark Wei434f2942012-08-24 11:54:02 -07001486 private void showErrorToast(String message) {
1487 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1488 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001489 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1490 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1491 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001492 }
1493
Paul Westbrookf97588b2012-03-20 11:11:37 -07001494 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001495 Bundle extras = intent.getExtras();
1496 if (extras == null) {
1497 extras = Bundle.EMPTY;
1498 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001499 final String action = intent.getAction();
1500 if (!mAttachmentsChanged) {
1501 long totalSize = 0;
1502 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1503 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1504 for (String uriString : uris) {
1505 final Uri uri = Uri.parse(uriString);
1506 long size = 0;
1507 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001508 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001509 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001510 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001511 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001512 }
1513 totalSize += size;
1514 }
1515 }
mindyp9a9e8d62012-10-03 12:24:07 -07001516 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1517 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1518 ArrayList<Parcelable> uris = extras
1519 .getParcelableArrayList(Intent.EXTRA_STREAM);
1520 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1521 for (Parcelable uri : uris) {
1522 try {
1523 attachments.add(mAttachmentsView.generateLocalAttachment((Uri) uri));
1524 } catch (AttachmentFailureException e) {
1525 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1526 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1527 getApplicationContext(),
1528 mAccount.settings.getMaxAttachmentSize());
1529 showErrorToast(getString
1530 (R.string.generic_attachment_problem, maxSize));
1531 }
1532 }
1533 totalSize += addAttachments(attachments);
1534 } else {
1535 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1536 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001537 try {
mindyp9a9e8d62012-10-03 12:24:07 -07001538 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001539 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001540 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001541 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001542 }
mindyp9a9e8d62012-10-03 12:24:07 -07001543 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001544 }
1545 }
1546
1547 if (totalSize > 0) {
1548 mAttachmentsChanged = true;
1549 updateSaveUi();
1550 }
1551 }
1552 }
1553
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001554 private void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
1555 mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1556 mShowQuotedText = true;
1557 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001558
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001559 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1560 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001561 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1562 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001563 }
1564
1565 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001566 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001567 boolean ccVisible = mCcBccView.isCcVisible();
1568 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001569 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001570 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001571 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001572 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001573 : R.string.add_bcc_label));
1574 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001575 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001576 }
1577 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001578 }
1579
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001580 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001581 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1582 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1583 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1584 if (showCc || showBcc) {
1585 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001586 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001587 }
1588 }
1589
Mindy Pereira013194c2012-01-06 15:09:33 -08001590 /**
1591 * Add attachment and update the compose area appropriately.
1592 * @param data
1593 */
1594 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001595 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1596 }
1597
Andy Huang5c5fd572012-04-08 18:19:29 -07001598 public void addAttachmentAndUpdateView(Uri contentUri) {
1599 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001600 return;
1601 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001602 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001603 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1604 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001605 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001606 showErrorToast(getResources().getString(
1607 e.getErrorRes(),
1608 AttachmentUtils.convertToHumanReadableSize(
1609 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001610 }
1611 }
1612
1613 public void addAttachmentAndUpdateView(Attachment attachment) {
1614 try {
Mark Wei434f2942012-08-24 11:54:02 -07001615 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001616 if (size > 0) {
1617 mAttachmentsChanged = true;
1618 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001619 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001620 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001621 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001622 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001623 }
1624 }
1625
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001626 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001627 // Don't populate the address if this is a forward.
1628 if (action == ComposeActivity.FORWARD) {
1629 return;
1630 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001631 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001632 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001633
Paul Westbrook6d2442b2013-07-17 17:51:51 -07001634 // TODO: This should be private. This method shouldn't be used by ComposeActivityTests, as
1635 // it doesn't setup the state of the activity correctly
Mindy Pereira818143e2012-01-11 13:59:49 -08001636 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001637 void initReplyRecipients(final Message refMessage, final int action) {
Tony Mantler9016a5e2013-07-19 11:54:17 -07001638 String[] sentToAddresses = refMessage.getToAddressesUnescaped();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001639 final Collection<String> toAddresses;
Tony Mantler89de9eb2013-07-25 11:43:58 -07001640 final String[] replyToAddresses = refMessage.getReplyToAddressesUnescaped();
1641 String replyToAddress = replyToAddresses.length > 0 ? replyToAddresses[0] : null;
1642 final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1643 final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
1644
mindyp65b06f52012-11-21 10:35:08 -08001645 // If there is no reply to address, the reply to address is the sender.
Tony Mantler89de9eb2013-07-25 11:43:58 -07001646 if (TextUtils.isEmpty(replyToAddress)) {
1647 replyToAddress = fromAddress;
mindyp65b06f52012-11-21 10:35:08 -08001648 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001649
1650 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1651 // Cc list is the union of the To and Cc recipients of the original
1652 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001653 // already on the To list.
1654 if (action == ComposeActivity.REPLY) {
Tony Mantler89de9eb2013-07-25 11:43:58 -07001655 toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001656 addToAddresses(toAddresses);
1657 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001658 final Set<String> ccAddresses = Sets.newHashSet();
Tony Mantler89de9eb2013-07-25 11:43:58 -07001659 toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001660 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001661 addRecipients(ccAddresses, sentToAddresses);
Tony Mantler9016a5e2013-07-19 11:54:17 -07001662 addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001663 addCcAddresses(ccAddresses, toAddresses);
1664 }
1665 }
1666
1667 private void addToAddresses(Collection<String> addresses) {
1668 addAddressesToList(addresses, mTo);
1669 }
1670
1671 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001672 addCcAddressesToList(tokenizeAddressList(addresses),
1673 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001674 }
1675
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001676 private void addBccAddresses(Collection<String> addresses) {
1677 addAddressesToList(addresses, mBcc);
1678 }
1679
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001680 @VisibleForTesting
1681 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1682 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1683 String address;
1684
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001685 if (compareToList == null) {
1686 for (Rfc822Token[] tokens : addresses) {
1687 for (int i = 0; i < tokens.length; i++) {
1688 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001689 list.append(address + END_TOKEN);
1690 }
1691 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001692 } else {
1693 HashSet<String> compareTo = convertToHashSet(compareToList);
1694 for (Rfc822Token[] tokens : addresses) {
1695 for (int i = 0; i < tokens.length; i++) {
1696 address = tokens[i].toString();
1697 // Check if this is a duplicate:
1698 if (!compareTo.contains(tokens[i].getAddress())) {
1699 // Get the address here
1700 list.append(address + END_TOKEN);
1701 }
1702 }
1703 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001704 }
1705 }
1706
Scott Kennedyff8553f2013-04-05 20:57:44 -07001707 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
1708 final HashSet<String> hash = new HashSet<String>();
1709 for (final Rfc822Token[] tokens : list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001710 for (int i = 0; i < tokens.length; i++) {
1711 hash.add(tokens[i].getAddress());
1712 }
1713 }
1714 return hash;
1715 }
1716
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001717 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1718 @VisibleForTesting
1719 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1720
1721 for (String address: addresses) {
1722 tokenized.add(Rfc822Tokenizer.tokenize(address));
1723 }
1724 return tokenized;
1725 }
1726
1727 @VisibleForTesting
1728 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1729 for (String address : addresses) {
1730 addAddressToList(address, list);
1731 }
1732 }
1733
Scott Kennedyff8553f2013-04-05 20:57:44 -07001734 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001735 if (address == null || list == null)
1736 return;
1737
Scott Kennedyff8553f2013-04-05 20:57:44 -07001738 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001739
1740 for (int i = 0; i < tokens.length; i++) {
1741 list.append(tokens[i] + END_TOKEN);
1742 }
1743 }
1744
1745 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001746 protected Collection<String> initToRecipients(final String fullSenderAddress,
1747 final String replyToAddress, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001748 // The To recipient is the reply-to address specified in the original
1749 // message, unless it is:
1750 // the current user OR a custom from of the current user, in which case
1751 // it's the To recipient list of the original message.
1752 // OR missing, in which case use the sender of the original message
1753 Set<String> toAddresses = Sets.newHashSet();
mindyp65b06f52012-11-21 10:35:08 -08001754 if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001755 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001756 } else {
mindyp65b06f52012-11-21 10:35:08 -08001757 // In this case, the user is replying to a message in which their
1758 // current account or one of their custom from addresses is the only
1759 // recipient and they sent the original message.
1760 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
1761 && recipientMatchesThisAccount(inToAddresses[0])) {
1762 toAddresses.add(inToAddresses[0]);
1763 return toAddresses;
1764 }
1765 // This happens if the user replies to a message they originally
1766 // wrote. In this case, "reply" really means "re-send," so we
1767 // target the original recipients. This works as expected even
1768 // if the user sent the original message to themselves.
1769 for (String address : inToAddresses) {
1770 if (!recipientMatchesThisAccount(address)) {
1771 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08001772 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001773 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001774 }
1775 return toAddresses;
1776 }
1777
Scott Kennedyff8553f2013-04-05 20:57:44 -07001778 private void addRecipients(final Set<String> recipients, final String[] addresses) {
1779 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001780 // Do not add this account, or any of its custom from addresses, to
1781 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001782 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08001783 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001784 recipients.add(email.replace("\"\"", ""));
1785 }
1786 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001787 }
1788
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001789 /**
1790 * A recipient matches this account if it has the same address as the
1791 * currently selected account OR one of the custom from addresses associated
1792 * with the currently selected account.
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001793 * @param recipientAddress address we are comparing with the currently selected account
1794 * @return
1795 */
mindyp5ee5d692012-11-19 16:02:16 -08001796 protected boolean recipientMatchesThisAccount(String recipientAddress) {
1797 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08001798 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001799 }
1800
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001801 /**
1802 * Returns a formatted subject string with the appropriate prefix for the action type.
1803 * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
1804 */
1805 public static String buildFormattedSubject(Resources res, String subject, int action) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001806 String prefix;
1807 String correctedSubject = null;
1808 if (action == ComposeActivity.COMPOSE) {
1809 prefix = "";
1810 } else if (action == ComposeActivity.FORWARD) {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001811 prefix = res.getString(R.string.forward_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001812 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001813 prefix = res.getString(R.string.reply_subject_label);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001814 }
1815
1816 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07001817 if (!TextUtils.isEmpty(subject)
1818 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001819 correctedSubject = subject;
1820 } else {
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001821 correctedSubject = String.format(
1822 res.getString(R.string.formatted_subject), prefix, subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001823 }
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07001824
1825 return correctedSubject;
1826 }
1827
1828 private void setSubject(Message refMessage, int action) {
1829 mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001830 }
1831
Mindy Pereira818143e2012-01-11 13:59:49 -08001832 private void initRecipients() {
1833 setupRecipients(mTo);
1834 setupRecipients(mCc);
1835 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001836 }
1837
Mindy Pereira818143e2012-01-11 13:59:49 -08001838 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001839 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001840 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001841 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001842 int offset = accountName.indexOf("@") + 1;
1843 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001844 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001845 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001846 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001847 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001848 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001849 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001850 }
1851
1852 @Override
1853 public void onClick(View v) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001854 final int id = v.getId();
1855 if (id == R.id.add_cc_bcc) {
1856 // Verify that cc/ bcc aren't showing.
1857 // Animate in cc/bcc.
1858 showCcBccViews();
1859 } else if (id == R.id.add_photo_attachment) {
1860 doAttach(MIME_TYPE_PHOTO);
1861 } else if (id == R.id.add_video_attachment) {
1862 doAttach(MIME_TYPE_VIDEO);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001863 }
1864 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001865
1866 @Override
1867 public boolean onCreateOptionsMenu(Menu menu) {
1868 super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07001869 // Don't render any menu items when there are no accounts.
1870 if (mAccounts == null || mAccounts.length == 0) {
1871 return true;
1872 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001873 MenuInflater inflater = getMenuInflater();
1874 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08001875
1876 /*
1877 * Start save in the correct enabled state.
1878 * 1) If a user launches compose from within gmail, save is disabled
1879 * until they add something, at which point, save is enabled, auto save
1880 * on exit; if the user empties everything, save is disabled, exiting does not
1881 * auto-save
1882 * 2) if a user replies/ reply all/ forwards from within gmail, save is
1883 * disabled until they change something, at which point, save is
1884 * enabled, auto save on exit; if the user empties everything, save is
1885 * disabled, exiting does not auto-save.
1886 * 3) If a user launches compose from another application and something
1887 * gets populated (attachments, recipients, body, subject, etc), save is
1888 * enabled, auto save on exit; if the user empties everything, save is
1889 * disabled, exiting does not auto-save
1890 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08001891 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08001892 String action = getIntent() != null ? getIntent().getAction() : null;
Andy Huang9f855d62013-05-30 17:15:03 -07001893 enableSave(mInnerSavedState != null ?
1894 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
mindyp1d7e9142012-11-21 13:54:30 -08001895 : (Intent.ACTION_SEND.equals(action)
1896 || Intent.ACTION_SEND_MULTIPLE.equals(action)
1897 || Intent.ACTION_SENDTO.equals(action)
1898 || shouldSave()));
1899
Mindy Pereira82cc5662012-01-09 17:29:30 -08001900 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001901 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1902 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1903 if (helpItem != null) {
1904 helpItem.setVisible(mAccount != null
1905 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1906 }
1907 if (sendFeedbackItem != null) {
1908 sendFeedbackItem.setVisible(mAccount != null
1909 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1910 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001911 return true;
1912 }
1913
1914 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001915 public boolean onPrepareOptionsMenu(Menu menu) {
1916 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001917 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001918 // Its possible there is a menu item OR a button.
1919 boolean ccFieldVisible = mCc.isShown();
1920 boolean bccFieldVisible = mBcc.isShown();
1921 if (!ccFieldVisible || !bccFieldVisible) {
1922 ccBcc.setVisible(true);
1923 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1924 : R.string.add_bcc_label));
1925 } else {
1926 ccBcc.setVisible(false);
1927 }
1928 }
1929 return true;
1930 }
1931
1932 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001933 public boolean onOptionsItemSelected(MenuItem item) {
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001934 final int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001935 boolean handled = true;
Scott Kennedy2b9d80e2013-07-30 23:03:45 -07001936 if (id == R.id.add_photo_attachment) {
1937 doAttach(MIME_TYPE_PHOTO);
1938 } else if (id == R.id.add_video_attachment) {
1939 doAttach(MIME_TYPE_VIDEO);
1940 } else if (id == R.id.add_cc_bcc) {
1941 showCcBccViews();
1942 } else if (id == R.id.save) {
1943 doSave(true);
1944 } else if (id == R.id.send) {
1945 doSend();
1946 } else if (id == R.id.discard) {
1947 doDiscard();
1948 } else if (id == R.id.settings) {
1949 Utils.showSettings(this, mAccount);
1950 } else if (id == android.R.id.home) {
1951 onAppUpPressed();
1952 } else if (id == R.id.help_info_menu_item) {
1953 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
1954 } else if (id == R.id.feedback_menu_item) {
1955 Utils.sendFeedback(this, mAccount, false);
1956 } else {
1957 handled = false;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001958 }
1959 return !handled ? super.onOptionsItemSelected(item) : handled;
1960 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001961
Mindy Pereirab199d172012-08-13 11:04:03 -07001962 @Override
1963 public void onBackPressed() {
1964 // If we are showing the wait fragment, just exit.
1965 if (getWaitFragment() != null) {
1966 finish();
1967 } else {
1968 super.onBackPressed();
1969 }
1970 }
1971
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001972 /**
1973 * Carries out the "up" action in the action bar.
1974 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001975 private void onAppUpPressed() {
1976 if (mLaunchedFromEmail) {
1977 // If this was started from Gmail, simply treat app up as the system back button, so
1978 // that the last view is restored.
1979 onBackPressed();
1980 return;
1981 }
1982
1983 // Fire the main activity to ensure it launches the "top" screen of mail.
1984 // Since the main Activity is singleTask, it should revive that task if it was already
1985 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07001986 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001987 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
1988 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
1989 startActivity(mailIntent);
1990 finish();
1991 }
1992
Mindy Pereira33fe9082012-01-09 16:24:30 -08001993 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07001994 sendOrSaveWithSanityChecks(false, true, false, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001995 }
1996
Mindy Pereira48e31b02012-05-30 13:12:24 -07001997 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07001998 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07001999 }
2000
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002001 @VisibleForTesting
2002 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002003 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002004 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2005 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002006 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2007 }
2008
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002009 @VisibleForTesting
2010 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002011 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002012 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002013 public final SendOrSaveCallback mSendOrSaveCallback;
2014 @VisibleForTesting
2015 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08002016 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002017
2018 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08002019 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002020 mContext = context;
2021 mSendOrSaveCallback = callback;
2022 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08002023 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002024 }
2025
2026 @Override
2027 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002028 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002029
Mindy Pereira92551d02012-04-05 11:31:12 -07002030 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002031 Message message = mSendOrSaveCallback.getMessage();
2032 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002033 // If a previous draft has been saved, in an account that is different
2034 // than what the user wants to send from, remove the old draft, and treat this
2035 // as a new message
mindyp44a63392012-11-05 12:05:16 -08002036 if (mExistingDraftAccount != null
2037 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002038 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2039 ContentResolver resolver = mContext.getContentResolver();
2040 ContentValues values = new ContentValues();
2041 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08002042 if (mExistingDraftAccount.account.expungeMessageUri != null) {
2043 new ContentProviderTask.UpdateTask()
2044 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2045 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002046 } else {
2047 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002048 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002049 // reset messageId to 0, so a new message will be created
2050 messageId = UIProvider.INVALID_MESSAGE_ID;
2051 }
2052 }
2053
2054 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07002055 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002056
2057 if (!sendOrSaveMessage.mSave) {
Tony Mantler9f324232013-08-08 14:24:30 -07002058 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002059 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Tony Mantler9f324232013-08-08 14:24:30 -07002060 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002061 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Tony Mantler9f324232013-08-08 14:24:30 -07002062 incrementRecipientsTimesContacted(mContext,
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002063 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2064 }
2065 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2066 }
2067
Tony Mantler9f324232013-08-08 14:24:30 -07002068 private static void incrementRecipientsTimesContacted(final Context context,
2069 final String addressString) {
2070 if (TextUtils.isEmpty(addressString)) {
2071 return;
2072 }
2073 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2074 final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
2075 for (int i = 0; i < tokens.length;i++) {
2076 recipients.add(tokens[i].getAddress());
2077 }
2078 final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
2079 statsUpdater.updateWithAddress(recipients);
2080 }
2081
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002082 /**
2083 * Send or Save a message.
2084 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002085 private void sendOrSaveMessage(final long messageIdToSave,
2086 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002087 final ContentResolver resolver = mContext.getContentResolver();
2088 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2089
2090 final String accountMethod = sendOrSaveMessage.mSave ?
2091 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2092 UIProvider.AccountCallMethods.SEND_MESSAGE;
2093
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002094 try {
2095 if (updateExistingMessage) {
2096 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002097
Paul Westbrook013a23c2013-02-22 10:37:41 -08002098 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002099 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002100 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002101 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002102 final Bundle result = callAccountSendSaveMethod(resolver,
2103 selectedAccount.account, accountMethod, sendOrSaveMessage);
2104 if (result != null) {
2105 // If a non-null value was returned, then the provider handled the call
2106 // method
2107 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002108 }
2109 if (sendOrSaveMessage.mSave && messageUri != null) {
2110 final Cursor messageCursor = resolver.query(messageUri,
2111 UIProvider.MESSAGE_PROJECTION, null, null, null);
2112 if (messageCursor != null) {
2113 try {
2114 if (messageCursor.moveToFirst()) {
2115 // Broadcast notification that a new message has
2116 // been allocated
2117 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2118 new Message(messageCursor));
2119 }
2120 } finally {
2121 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002122 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002123 }
2124 }
2125 }
2126 } finally {
2127 // Close any opened file descriptors
2128 closeOpenedAttachmentFds(sendOrSaveMessage);
2129 }
2130 }
2131
Scott Kennedyff8553f2013-04-05 20:57:44 -07002132 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002133 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2134 if (openedFds != null) {
2135 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002136 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002137 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2138 if (fd != null) {
2139 try {
2140 fd.close();
2141 } catch (IOException e) {
2142 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002143 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002144 }
2145 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002146 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002147 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002148
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002149 /**
Andrew Sappersteinbc5e0dc2013-08-07 21:15:22 -07002150 * Use the {@link ContentResolver#call} method to send or save the message.
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002151 *
2152 * If this was successful, this method will return an non-null Bundle instance
2153 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002154 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2155 final Account account, final String method,
2156 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002157 // Copy all of the values from the content values to the bundle
2158 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2159 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2160
2161 for (Entry<String, Object> entry : valueSet) {
2162 final Object entryValue = entry.getValue();
2163 final String key = entry.getKey();
2164 if (entryValue instanceof String) {
2165 methodExtras.putString(key, (String)entryValue);
2166 } else if (entryValue instanceof Boolean) {
2167 methodExtras.putBoolean(key, (Boolean)entryValue);
2168 } else if (entryValue instanceof Integer) {
2169 methodExtras.putInt(key, (Integer)entryValue);
2170 } else if (entryValue instanceof Long) {
2171 methodExtras.putLong(key, (Long)entryValue);
2172 } else {
2173 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2174 entryValue.getClass().getName());
2175 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002176 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002177
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002178 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2179 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2180 if (fdMap != null) {
2181 methodExtras.putParcelable(
2182 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2183 }
2184
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002185 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002186 }
2187 }
2188
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002189 @VisibleForTesting
2190 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002191 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002192 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002193 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002194 @VisibleForTesting
2195 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002196 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002197 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002198
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002199 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2200 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002201 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002202 mValues = values;
2203 mRefMessageId = refMessageId;
2204 mSave = save;
2205 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002206
2207 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002208 }
2209
2210 int requestId() {
2211 return mRequestId;
2212 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002213
2214 Bundle attachmentFds() {
2215 return mAttachmentFds;
2216 }
2217
2218 /**
2219 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2220 * called before the ComposeActivity finishes.
2221 * Note: The caller is responsible for closing these file descriptors.
2222 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002223 private static Bundle initializeAttachmentFds(final Context context,
2224 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002225 if (attachments == null || attachments.size() == 0) {
2226 return null;
2227 }
2228
2229 final Bundle result = new Bundle(attachments.size());
2230 final ContentResolver resolver = context.getContentResolver();
2231
2232 for (Attachment attachment : attachments) {
2233 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2234 continue;
2235 }
2236
2237 ParcelFileDescriptor fileDescriptor;
2238 try {
2239 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2240 } catch (FileNotFoundException e) {
2241 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2242 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002243 } catch (SecurityException e) {
2244 // We have encountered a security exception when attempting to open the file
2245 // specified by the content uri. If the attachment has been cached, this
2246 // isn't a problem, as even through the original permission may have been
2247 // revoked, we have cached the file. This will happen when saving/sending
2248 // a previously saved draft.
2249 // TODO(markwei): Expose whether the attachment has been cached through the
2250 // attachment object. This would allow us to limit when the log is made, as
2251 // if the attachment has been cached, this really isn't an error
2252 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2253 // Just set the file descriptor to null, as the underlying provider needs
2254 // to handle the file descriptor not being set.
2255 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002256 }
2257
2258 if (fileDescriptor != null) {
2259 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2260 }
2261 }
2262
2263 return result;
2264 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002265 }
2266
2267 /**
2268 * Get the to recipients.
2269 */
2270 public String[] getToAddresses() {
2271 return getAddressesFromList(mTo);
2272 }
2273
2274 /**
2275 * Get the cc recipients.
2276 */
2277 public String[] getCcAddresses() {
2278 return getAddressesFromList(mCc);
2279 }
2280
2281 /**
2282 * Get the bcc recipients.
2283 */
2284 public String[] getBccAddresses() {
2285 return getAddressesFromList(mBcc);
2286 }
2287
2288 public String[] getAddressesFromList(RecipientEditTextView list) {
2289 if (list == null) {
2290 return new String[0];
2291 }
2292 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2293 int count = tokens.length;
2294 String[] result = new String[count];
2295 for (int i = 0; i < count; i++) {
2296 result[i] = tokens[i].toString();
2297 }
2298 return result;
2299 }
2300
2301 /**
2302 * Check for invalid email addresses.
2303 * @param to String array of email addresses to check.
2304 * @param wrongEmailsOut Emails addresses that were invalid.
2305 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002306 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002307 if (mValidator == null) {
2308 return;
2309 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002310 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002311 if (!mValidator.isValid(email)) {
2312 wrongEmailsOut.add(email);
2313 }
2314 }
2315 }
2316
Tony Mantler2558b502013-07-09 10:53:34 -07002317 public static class RecipientErrorDialogFragment extends DialogFragment {
2318 public static RecipientErrorDialogFragment newInstance(final String message) {
2319 final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2320 final Bundle args = new Bundle(1);
2321 args.putString("message", message);
2322 frag.setArguments(args);
2323 return frag;
2324 }
2325
2326 @Override
2327 public Dialog onCreateDialog(Bundle savedInstanceState) {
2328 final String message = getArguments().getString("message");
2329 return new AlertDialog.Builder(getActivity()).setMessage(message).setTitle(
2330 R.string.recipient_error_dialog_title)
2331 .setIconAttribute(android.R.attr.alertDialogIcon)
2332 .setPositiveButton(
2333 R.string.ok, new Dialog.OnClickListener() {
2334 @Override
2335 public void onClick(DialogInterface dialog, int which) {
2336 ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2337 }
2338 }).create();
2339 }
2340 }
2341
2342 private void finishRecipientErrorDialog() {
2343 // after the user dismisses the recipient error
2344 // dialog we want to make sure to refocus the
2345 // recipient to field so they can fix the issue
2346 // easily
2347 if (mTo != null) {
2348 mTo.requestFocus();
2349 }
2350 }
2351
Mindy Pereira82cc5662012-01-09 17:29:30 -08002352 /**
2353 * Show an error because the user has entered an invalid recipient.
2354 * @param message
2355 */
Tony Mantler2558b502013-07-09 10:53:34 -07002356 private void showRecipientErrorDialog(final String message) {
2357 final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2358 frag.show(getFragmentManager(), "recipient error");
Mindy Pereira82cc5662012-01-09 17:29:30 -08002359 }
2360
2361 /**
2362 * Update the state of the UI based on whether or not the current draft
2363 * needs to be saved and the message is not empty.
2364 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002365 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002366 if (mSave != null) {
2367 mSave.setEnabled((shouldSave() && !isBlank()));
2368 }
2369 }
2370
2371 /**
2372 * Returns true if we need to save the current draft.
2373 */
2374 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002375 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002376 // The message should only be saved if:
2377 // It hasn't been sent AND
2378 // Some text has been added to the message OR
2379 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002380 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002381 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002382 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002383 }
2384 }
2385
2386 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002387 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002388 * @return boolean
2389 */
2390 public boolean isBlank() {
2391 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002392 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2393 mBodyView.getText().toString()) == 0)
2394 && mTo.length() == 0
2395 && mCc.length() == 0 && mBcc.length() == 0
2396 && mAttachmentsView.getAttachments().size() == 0;
2397 }
2398
2399 @VisibleForTesting
2400 protected int getSignatureStartPosition(String signature, String bodyText) {
2401 int startPos = -1;
2402
2403 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2404 return startPos;
2405 }
2406
2407 int bodyLength = bodyText.length();
2408 int signatureLength = signature.length();
2409 String printableVersion = convertToPrintableSignature(signature);
2410 int printableLength = printableVersion.length();
2411
2412 if (bodyLength >= printableLength
2413 && bodyText.substring(bodyLength - printableLength)
2414 .equals(printableVersion)) {
2415 startPos = bodyLength - printableLength;
2416 } else if (bodyLength >= signatureLength
2417 && bodyText.substring(bodyLength - signatureLength)
2418 .equals(signature)) {
2419 startPos = bodyLength - signatureLength;
2420 }
2421 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002422 }
2423
2424 /**
2425 * Allows any changes made by the user to be ignored. Called when the user
2426 * decides to discard a draft.
2427 */
2428 private void discardChanges() {
2429 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002430 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002431 mReplyFromChanged = false;
2432 }
2433
2434 /**
Mindy Pereira181df782012-03-01 13:32:44 -08002435 * @param save
2436 * @param showToast
2437 * @return Whether the send or save succeeded.
2438 */
2439 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002440 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002441 if (mAccounts == null || mAccount == null) {
2442 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002443 if (autoSend) {
2444 finish();
2445 }
Mark Wei009b3712012-10-18 18:07:50 -07002446 return false;
2447 }
2448
Scott Kennedyff8553f2013-04-05 20:57:44 -07002449 final String[] to, cc, bcc;
Mindy Pereira181df782012-03-01 13:32:44 -08002450 if (orientationChanged) {
2451 to = cc = bcc = new String[0];
2452 } else {
2453 to = getToAddresses();
2454 cc = getCcAddresses();
2455 bcc = getBccAddresses();
2456 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002457
Mindy Pereira181df782012-03-01 13:32:44 -08002458 // Don't let the user send to nobody (but it's okay to save a message
2459 // with no recipients)
2460 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2461 showRecipientErrorDialog(getString(R.string.recipient_needed));
2462 return false;
2463 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002464
Mindy Pereira181df782012-03-01 13:32:44 -08002465 List<String> wrongEmails = new ArrayList<String>();
2466 if (!save) {
2467 checkInvalidEmails(to, wrongEmails);
2468 checkInvalidEmails(cc, wrongEmails);
2469 checkInvalidEmails(bcc, wrongEmails);
2470 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002471
Mindy Pereira181df782012-03-01 13:32:44 -08002472 // Don't let the user send an email with invalid recipients
2473 if (wrongEmails.size() > 0) {
2474 String errorText = String.format(getString(R.string.invalid_recipient),
2475 wrongEmails.get(0));
2476 showRecipientErrorDialog(errorText);
2477 return false;
2478 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002479
Mindy Pereira181df782012-03-01 13:32:44 -08002480 // Show a warning before sending only if there are no attachments.
2481 if (!save) {
2482 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2483 boolean warnAboutEmptySubject = isSubjectEmpty();
Tony Mantler2558b502013-07-09 10:53:34 -07002484 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002485
Mindy Pereira181df782012-03-01 13:32:44 -08002486 // A warning about an empty body may not be warranted when
2487 // forwarding mails, since a common use case is to forward
2488 // quoted text and not append any more text.
2489 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002490
Mindy Pereira181df782012-03-01 13:32:44 -08002491 // When we bring up a dialog warning the user about a send,
2492 // assume that they accept sending the message. If they do not,
2493 // the dialog listener is required to enable sending again.
2494 if (warnAboutEmptySubject) {
Tony Mantler2558b502013-07-09 10:53:34 -07002495 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, save,
2496 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002497 return true;
2498 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002499
Mindy Pereira181df782012-03-01 13:32:44 -08002500 if (warnAboutEmptyBody) {
Tony Mantler2558b502013-07-09 10:53:34 -07002501 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, save,
2502 showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002503 return true;
2504 }
2505 }
2506 // Ask for confirmation to send (if always required)
2507 if (showSendConfirmation()) {
Tony Mantler2558b502013-07-09 10:53:34 -07002508 showSendConfirmDialog(R.string.confirm_send_message, save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002509 return true;
2510 }
2511 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002512
Tony Mantler2558b502013-07-09 10:53:34 -07002513 sendOrSave(save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002514 return true;
2515 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002516
Mindy Pereira181df782012-03-01 13:32:44 -08002517 /**
2518 * Returns a boolean indicating whether warnings should be shown for empty
2519 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002520 *
Mindy Pereira181df782012-03-01 13:32:44 -08002521 * @return True if a warning should be shown for empty text fields
2522 */
2523 protected boolean showEmptyTextWarnings() {
2524 return mAttachmentsView.getAttachments().size() == 0;
2525 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002526
Mindy Pereira181df782012-03-01 13:32:44 -08002527 /**
2528 * Returns a boolean indicating whether the user should confirm each send
2529 *
2530 * @return True if a warning should be on each send
2531 */
2532 protected boolean showSendConfirmation() {
2533 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2534 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002535
Tony Mantler2558b502013-07-09 10:53:34 -07002536 public static class SendConfirmDialogFragment extends DialogFragment {
2537 public static SendConfirmDialogFragment newInstance(final int messageId,
2538 final boolean save, final boolean showToast) {
2539 final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2540 final Bundle args = new Bundle(3);
2541 args.putInt("messageId", messageId);
2542 args.putBoolean("save", save);
2543 args.putBoolean("showToast", showToast);
2544 frag.setArguments(args);
2545 return frag;
Mindy Pereira181df782012-03-01 13:32:44 -08002546 }
Tony Mantler2558b502013-07-09 10:53:34 -07002547
2548 @Override
2549 public Dialog onCreateDialog(Bundle savedInstanceState) {
2550 final int messageId = getArguments().getInt("messageId");
2551 final boolean save = getArguments().getBoolean("save");
2552 final boolean showToast = getArguments().getBoolean("showToast");
2553
2554 return new AlertDialog.Builder(getActivity())
2555 .setMessage(messageId)
2556 .setTitle(R.string.confirm_send_title)
2557 .setIconAttribute(android.R.attr.alertDialogIcon)
2558 .setPositiveButton(R.string.send,
2559 new DialogInterface.OnClickListener() {
Scott Kennedyaa27bc02013-08-02 08:47:26 -07002560 @Override
Tony Mantler2558b502013-07-09 10:53:34 -07002561 public void onClick(DialogInterface dialog, int whichButton) {
2562 ((ComposeActivity)getActivity()).finishSendConfirmDialog(save,
2563 showToast);
2564 }
2565 })
2566 .create();
2567 }
2568 }
2569
2570 private void finishSendConfirmDialog(final boolean save, final boolean showToast) {
2571 sendOrSave(save, showToast);
2572 }
2573
2574 private void showSendConfirmDialog(final int messageId, final boolean save,
2575 final boolean showToast) {
2576 final DialogFragment frag = SendConfirmDialogFragment.newInstance(messageId, save,
2577 showToast);
2578 frag.show(getFragmentManager(), "send confirm");
Mindy Pereira181df782012-03-01 13:32:44 -08002579 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002580
Mindy Pereira181df782012-03-01 13:32:44 -08002581 /**
2582 * Returns whether the ComposeArea believes there is any text in the body of
2583 * the composition. TODO: When ComposeArea controls the Body as well, add
2584 * that here.
2585 */
2586 public boolean isBodyEmpty() {
2587 return !mQuotedTextView.isTextIncluded();
2588 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002589
Mindy Pereira181df782012-03-01 13:32:44 -08002590 /**
2591 * Test to see if the subject is empty.
2592 *
2593 * @return boolean.
2594 */
2595 // TODO: this will likely go away when composeArea.focus() is implemented
2596 // after all the widget control is moved over.
2597 public boolean isSubjectEmpty() {
2598 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2599 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002600
Mindy Pereira181df782012-03-01 13:32:44 -08002601 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002602 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07002603 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08002604 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
2605 ReplyFromAccount draftAccount) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08002606 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002607
Paul Westbrookb4931c62013-01-14 17:51:18 -08002608 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07002609
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002610 MessageModification.putToAddresses(values, message.getToAddresses());
2611 MessageModification.putCcAddresses(values, message.getCcAddresses());
2612 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002613
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002614 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07002615
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002616 MessageModification.putSubject(values, message.subject);
Paul Westbrookb4931c62013-01-14 17:51:18 -08002617 // Make sure to remove only the composing spans from the Spannable before saving.
2618 final String htmlBody = Html.toHtml(removeComposingSpans(body));
Paul Westbrook05b92b82012-04-20 13:29:37 -07002619
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002620 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2621 StringBuilder fullBody = new StringBuilder(htmlBody);
2622 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002623 // HTML gets converted to text for now
2624 final String text = quotedText.toString();
2625 if (QuotedTextView.containsQuotedText(text)) {
2626 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07002627 final int quoteStartPos = fullBody.length() + pos;
2628 fullBody.append(text);
2629 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07002630 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07002631 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002632 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002633 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2634 // This shouldn't happen, but just use what we have,
2635 // and don't do server-side expansion
2636 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002637 }
2638 }
Mindy Pereira002ff522012-05-30 10:31:26 -07002639 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07002640 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002641 if (refMessage != null) {
2642 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2643 MessageModification.putBodyHtml(values, fullBody.toString());
2644 }
2645 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08002646 MessageModification.putBody(values,
2647 Utils.convertHtmlToPlainText(fullBody.toString()).toString());
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002648 }
2649 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002650 MessageModification.putBodyHtml(values, fullBody.toString());
mindypc59dd822012-11-13 10:56:21 -08002651 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2652 .toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002653 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002654 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002655 if (!TextUtils.isEmpty(refMessageId)) {
2656 MessageModification.putRefMessageId(values, refMessageId);
2657 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002658 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2659 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08002660 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2661 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002662
Mindy Pereira181df782012-03-01 13:32:44 -08002663 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08002664 // Do the send/save action on the specified handler to avoid possible
2665 // ANRs
2666 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002667
Mindy Pereira181df782012-03-01 13:32:44 -08002668 return sendOrSaveMessage.requestId();
2669 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002670
Paul Westbrookb4931c62013-01-14 17:51:18 -08002671 /**
2672 * Removes any composing spans from the specified string. This will create a new
2673 * SpannableString instance, as to not modify the behavior of the EditText view.
2674 */
2675 private static SpannableString removeComposingSpans(Spanned body) {
2676 final SpannableString messageBody = new SpannableString(body);
2677 BaseInputConnection.removeComposingSpans(messageBody);
2678 return messageBody;
2679 }
2680
Mindy Pereira002ff522012-05-30 10:31:26 -07002681 private static int getDraftType(int mode) {
2682 int draftType = -1;
2683 switch (mode) {
2684 case ComposeActivity.COMPOSE:
2685 draftType = DraftType.COMPOSE;
2686 break;
2687 case ComposeActivity.REPLY:
2688 draftType = DraftType.REPLY;
2689 break;
2690 case ComposeActivity.REPLY_ALL:
2691 draftType = DraftType.REPLY_ALL;
2692 break;
2693 case ComposeActivity.FORWARD:
2694 draftType = DraftType.FORWARD;
2695 break;
2696 }
2697 return draftType;
2698 }
2699
Tony Mantler2558b502013-07-09 10:53:34 -07002700 private void sendOrSave(final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08002701 // Check if user is a monkey. Monkeys can compose and hit send
2702 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002703 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002704 return;
2705 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002706
Tony Mantler2558b502013-07-09 10:53:34 -07002707 final Spanned body = mBodyView.getEditableText();
2708
Mindy Pereira181df782012-03-01 13:32:44 -08002709 SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07002710 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08002711 private int mRestoredRequestId;
2712
Marc Blank0bbc8582012-04-23 15:07:57 -07002713 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002714 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002715 synchronized (mActiveTasks) {
2716 int numTasks = mActiveTasks.size();
2717 if (numTasks == 0) {
2718 // Start service so we won't be killed if this app is
2719 // put in the background.
2720 startService(new Intent(ComposeActivity.this, EmptyService.class));
2721 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002722
Mindy Pereira181df782012-03-01 13:32:44 -08002723 mActiveTasks.add(sendOrSaveTask);
2724 }
2725 if (sTestSendOrSaveCallback != null) {
2726 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2727 }
2728 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002729
Marc Blank0bbc8582012-04-23 15:07:57 -07002730 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002731 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2732 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002733 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08002734 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002735 mDraftId = message.id;
2736 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002737 if (sRequestMessageIdMap != null) {
2738 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2739 }
Mindy Pereira181df782012-03-01 13:32:44 -08002740 // Cache request message map, in case the process is killed
2741 saveRequestMap();
2742 }
2743 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002744 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002745 }
2746 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002747
Marc Blank0bbc8582012-04-23 15:07:57 -07002748 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002749 public Message getMessage() {
2750 synchronized (mDraftLock) {
2751 return mDraft;
2752 }
2753 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002754
Marc Blank0bbc8582012-04-23 15:07:57 -07002755 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002756 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07002757 // Update the last sent from account.
2758 if (mAccount != null) {
2759 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2760 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002761 if (success) {
2762 // Successfully sent or saved so reset change markers
2763 discardChanges();
2764 } else {
2765 // A failure happened with saving/sending the draft
2766 // TODO(pwestbro): add a better string that should be used
2767 // when failing to send or save
2768 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2769 .show();
2770 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002771
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002772 int numTasks;
2773 synchronized (mActiveTasks) {
2774 // Remove the task from the list of active tasks
2775 mActiveTasks.remove(task);
2776 numTasks = mActiveTasks.size();
2777 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002778
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002779 if (numTasks == 0) {
2780 // Stop service so we can be killed.
2781 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2782 }
2783 if (sTestSendOrSaveCallback != null) {
2784 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2785 }
2786 }
Mindy Pereira181df782012-03-01 13:32:44 -08002787 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002788
Mindy Pereira181df782012-03-01 13:32:44 -08002789 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07002790 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002791 String fromAddress = selectedAccount.name;
2792 if (selectedAccount == null || fromAddress == null) {
2793 // We don't have either the selected account or from address,
2794 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07002795 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002796 fromAddress = mAccount.name;
2797 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002798
Mindy Pereira181df782012-03-01 13:32:44 -08002799 if (mSendSaveTaskHandler == null) {
2800 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2801 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002802
Mindy Pereira181df782012-03-01 13:32:44 -08002803 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2804 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002805
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002806 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002807 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2808 mQuotedTextView.getQuotedTextIfIncluded(), callback,
mindyp44a63392012-11-05 12:05:16 -08002809 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002810
Mindy Pereira181df782012-03-01 13:32:44 -08002811 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
2812 mRecipient = selectedAccount.name;
2813 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002814 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002815
Mindy Pereira181df782012-03-01 13:32:44 -08002816 // Don't display the toast if the user is just changing the orientation,
2817 // but we still need to save the draft to the cursor because this is how we restore
2818 // the attachments when the configuration change completes.
2819 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2820 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2821 Toast.LENGTH_LONG).show();
2822 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002823
Mindy Pereira181df782012-03-01 13:32:44 -08002824 // Need to update variables here because the send or save completes
2825 // asynchronously even though the toast shows right away.
2826 discardChanges();
2827 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002828
Mindy Pereira181df782012-03-01 13:32:44 -08002829 // If we are sending, finish the activity
2830 if (!save) {
2831 finish();
2832 }
2833 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002834
Mindy Pereira181df782012-03-01 13:32:44 -08002835 /**
2836 * Save the state of the request messageid map. This allows for the Gmail
2837 * process to be killed, but and still allow for ComposeActivity instances
2838 * to be recreated correctly.
2839 */
2840 private void saveRequestMap() {
2841 // TODO: store the request map in user preferences.
2842 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002843
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002844 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002845 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2846 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002847 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002848 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08002849 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002850 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2851 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002852 }
2853
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002854 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002855 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002856 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07002857 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002858 }
2859 }
2860
Mindy Pereira326c6602012-01-04 15:32:42 -08002861 @Override
2862 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002863 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002864 if (position == ComposeActivity.REPLY) {
2865 mComposeMode = ComposeActivity.REPLY;
2866 } else if (position == ComposeActivity.REPLY_ALL) {
2867 mComposeMode = ComposeActivity.REPLY_ALL;
2868 } else if (position == ComposeActivity.FORWARD) {
2869 mComposeMode = ComposeActivity.FORWARD;
2870 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002871 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002872 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002873 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08002874 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08002875 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002876 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002877 boolean showCc = false;
2878 boolean showBcc = false;
2879 if (mDraft != null) {
2880 // Following desktop behavior, if the user has added a BCC
2881 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002882 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07002883 // Use the draft to determine what to populate.
2884 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002885 showCc = showBcc
2886 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08002887 }
2888 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07002889 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08002890 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07002891 }
2892 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002893 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002894 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002895 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08002896 return true;
2897 }
2898
Mindy Pereirab3112a22012-06-20 12:10:03 -07002899 @VisibleForTesting
2900 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08002901 // When switching between reply, reply all, forward,
2902 // follow the behavior of webview.
2903 // The contents of the following fields are cleared
2904 // so that they can be populated directly from the
2905 // ref message:
2906 // 1) Any recipient fields
2907 // 2) The subject
2908 mTo.setText("");
2909 mCc.setText("");
2910 mBcc.setText("");
2911 // Any edits to the subject are replaced with the original subject.
2912 mSubject.setText("");
2913
2914 // Any changes to the contents of the following fields are kept:
2915 // 1) Body
2916 // 2) Attachments
2917 // If the user made changes to attachments, keep their changes.
2918 if (!mAttachmentsChanged) {
2919 mAttachmentsView.deleteAllAttachments();
2920 }
2921 }
2922
Mindy Pereira326c6602012-01-04 15:32:42 -08002923 private class ComposeModeAdapter extends ArrayAdapter<String> {
2924
2925 private LayoutInflater mInflater;
2926
2927 public ComposeModeAdapter(Context context) {
2928 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2929 .getStringArray(R.array.compose_modes));
2930 }
2931
2932 private LayoutInflater getInflater() {
2933 if (mInflater == null) {
2934 mInflater = LayoutInflater.from(getContext());
2935 }
2936 return mInflater;
2937 }
2938
2939 @Override
2940 public View getView(int position, View convertView, ViewGroup parent) {
2941 if (convertView == null) {
2942 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2943 }
2944 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2945 return super.getView(position, convertView, parent);
2946 }
2947 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002948
2949 @Override
2950 public void onRespondInline(String text) {
2951 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07002952 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08002953 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08002954 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08002955 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08002956 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002957 }
2958
2959 /**
2960 * Append text to the body of the message. If there is no existing body
2961 * text, just sets the body to text.
2962 *
2963 * @param text
2964 * @param withSignature True to append a signature.
2965 */
2966 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002967 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002968 if (bodyText != null && bodyText.length() > 0) {
2969 bodyText.append(text);
2970 } else {
2971 setBody(text, withSignature);
2972 }
2973 }
2974
2975 /**
2976 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002977 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002978 * @param text
2979 * @param withSignature True to append a signature.
2980 */
2981 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002982 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002983 if (withSignature) {
2984 appendSignature();
2985 }
2986 }
2987
2988 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002989 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002990 boolean hasFocus = mBodyView.hasFocus();
mindyp27083062012-11-15 09:02:01 -08002991 int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
2992 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002993 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08002994 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002995 // Appending a signature does not count as changing text.
2996 mBodyView.removeTextChangedListener(this);
2997 mBodyView.append(convertToPrintableSignature(mSignature));
2998 mBodyView.addTextChangedListener(this);
2999 }
Mindy Pereira433b1982012-04-03 11:53:07 -07003000 if (hasFocus) {
3001 focusBody();
3002 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08003003 }
3004 }
3005
3006 private String convertToPrintableSignature(String signature) {
3007 String signatureResource = getResources().getString(R.string.signature);
3008 if (signature == null) {
3009 signature = "";
3010 }
3011 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08003012 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003013
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08003014 @Override
3015 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07003016 mReplyFromAccount = mFromSpinner.getCurrentAccount();
3017 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08003018 // Clear a signature, if there was one.
3019 mBodyView.removeTextChangedListener(this);
3020 String oldSignature = mSignature;
3021 String bodyText = getBody().getText().toString();
3022 if (!TextUtils.isEmpty(oldSignature)) {
3023 int pos = getSignatureStartPosition(oldSignature, bodyText);
3024 if (pos > -1) {
3025 mBodyView.setText(bodyText.substring(0, pos));
3026 }
3027 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07003028 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08003029 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08003030 // TODO: handle discarding attachments when switching accounts.
3031 // Only enable save for this draft if there is any other content
3032 // in the message.
3033 if (!isBlank()) {
3034 enableSave(true);
3035 }
3036 mReplyFromChanged = true;
3037 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003038 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08003039 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003040
3041 public void enableSave(boolean enabled) {
3042 if (mSave != null) {
3043 mSave.setEnabled(enabled);
3044 }
3045 }
3046
Tony Mantler2558b502013-07-09 10:53:34 -07003047 public static class DiscardConfirmDialogFragment extends DialogFragment {
3048 @Override
3049 public Dialog onCreateDialog(Bundle savedInstanceState) {
3050 return new AlertDialog.Builder(getActivity())
3051 .setMessage(R.string.confirm_discard_text)
3052 .setPositiveButton(R.string.discard,
3053 new DialogInterface.OnClickListener() {
3054 @Override
3055 public void onClick(DialogInterface dialog, int which) {
3056 ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3057 }
3058 })
Tony Mantler2b215b72013-07-31 10:20:46 -07003059 .setNegativeButton(R.string.cancel, null)
Tony Mantler2558b502013-07-09 10:53:34 -07003060 .create();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003061 }
3062 }
3063
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003064 private void doDiscard() {
Tony Mantler2558b502013-07-09 10:53:34 -07003065 final DialogFragment frag = new DiscardConfirmDialogFragment();
3066 frag.show(getFragmentManager(), "discard confirm");
Mindy Pereiraefe3d252012-03-01 14:20:44 -08003067 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003068 /**
3069 * Effectively discard the current message.
3070 *
3071 * This method is either invoked from the menu or from the dialog
3072 * once the user has confirmed that they want to discard the message.
Mindy Pereira82cc5662012-01-09 17:29:30 -08003073 */
Tony Mantler2558b502013-07-09 10:53:34 -07003074 private void doDiscardWithoutConfirmation() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08003075 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08003076 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3077 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07003078 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07003079 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003080 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3081 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07003082 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08003083 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08003084 // This is not strictly necessary (since we should not try to
3085 // save the draft after calling this) but it ensures that if we
3086 // do save again for some reason we make a new draft rather than
3087 // trying to resave an expunged draft.
3088 mDraftId = UIProvider.INVALID_MESSAGE_ID;
3089 }
3090 }
3091
Tony Mantler2558b502013-07-09 10:53:34 -07003092 // Display a toast to let the user know
3093 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
Mindy Pereira82cc5662012-01-09 17:29:30 -08003094
3095 // This prevents the draft from being saved in onPause().
3096 discardChanges();
3097 finish();
3098 }
3099
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003100 private void saveIfNeeded() {
3101 if (mAccount == null) {
3102 // We have not chosen an account yet so there's no way that we can save. This is ok,
3103 // though, since we are saving our state before AccountsActivity is activated. Thus, the
3104 // user has not interacted with us yet and there is no real state to save.
3105 return;
3106 }
3107
3108 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003109 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003110 }
3111 }
3112
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003113 @Override
3114 public void onAttachmentDeleted() {
3115 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003116 // If we are showing any attachments, make sure we have an upper
3117 // divider.
3118 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003119 updateSaveUi();
3120 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003121
mindyp40882432012-09-06 11:07:40 -07003122 @Override
3123 public void onAttachmentAdded() {
3124 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3125 mAttachmentsView.focusLastAttachment();
3126 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003127
3128 /**
3129 * This is called any time one of our text fields changes.
3130 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003131 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003132 public void afterTextChanged(Editable s) {
3133 mTextChanged = true;
3134 updateSaveUi();
3135 }
3136
3137 @Override
3138 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3139 // Do nothing.
3140 }
3141
Marc Blank0bbc8582012-04-23 15:07:57 -07003142 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003143 public void onTextChanged(CharSequence s, int start, int before, int count) {
3144 // Do nothing.
3145 }
3146
3147
3148 // There is a big difference between the text associated with an address changing
3149 // to add the display name or to format properly and a recipient being added or deleted.
3150 // Make sure we only notify of changes when a recipient has been added or deleted.
3151 private class RecipientTextWatcher implements TextWatcher {
3152 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3153
3154 private RecipientEditTextView mView;
3155
3156 private TextWatcher mListener;
3157
3158 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3159 mView = view;
3160 mListener = listener;
3161 }
3162
3163 @Override
3164 public void afterTextChanged(Editable s) {
3165 if (hasChanged()) {
3166 mListener.afterTextChanged(s);
3167 }
3168 }
3169
3170 private boolean hasChanged() {
3171 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
3172 int totalCount = currRecips.length;
3173 int totalPrevCount = 0;
3174 for (Entry<String, Integer> entry : mContent.entrySet()) {
3175 totalPrevCount += entry.getValue();
3176 }
3177 if (totalCount != totalPrevCount) {
3178 return true;
3179 }
3180
3181 for (String recip : currRecips) {
3182 if (!mContent.containsKey(recip)) {
3183 return true;
3184 } else {
3185 int count = mContent.get(recip) - 1;
3186 if (count < 0) {
3187 return true;
3188 } else {
3189 mContent.put(recip, count);
3190 }
3191 }
3192 }
3193 return false;
3194 }
3195
3196 private String[] tokenizeRecips(String[] recips) {
3197 // Tokenize them all and put them in the list.
3198 String[] recipAddresses = new String[recips.length];
3199 for (int i = 0; i < recips.length; i++) {
3200 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
3201 }
3202 return recipAddresses;
3203 }
3204
3205 @Override
3206 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3207 String[] recips = tokenizeRecips(getAddressesFromList(mView));
3208 for (String recip : recips) {
3209 if (!mContent.containsKey(recip)) {
3210 mContent.put(recip, 1);
3211 } else {
3212 mContent.put(recip, (mContent.get(recip)) + 1);
3213 }
3214 }
3215 }
3216
3217 @Override
3218 public void onTextChanged(CharSequence s, int start, int before, int count) {
3219 // Do nothing.
3220 }
3221 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003222
3223 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3224 if (sTestSendOrSaveCallback != null && testCallback != null) {
3225 throw new IllegalStateException("Attempting to register more than one test callback");
3226 }
3227 sTestSendOrSaveCallback = testCallback;
3228 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003229
3230 @VisibleForTesting
3231 protected ArrayList<Attachment> getAttachments() {
3232 return mAttachmentsView.getAttachments();
3233 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003234
3235 @Override
3236 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3237 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003238 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3239 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3240 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003241 case REFERENCE_MESSAGE_LOADER:
3242 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3243 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003244 case LOADER_ACCOUNT_CURSOR:
3245 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3246 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003247 }
3248 return null;
3249 }
3250
3251 @Override
3252 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003253 int id = loader.getId();
3254 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003255 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003256 if (data != null && data.moveToFirst()) {
3257 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003258 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003259 initFromRefMessage(mComposeMode);
3260 finishSetup(mComposeMode, intent, null);
3261 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003262 String to = intent.getStringExtra(EXTRA_TO);
3263 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003264 mRefMessage.setTo(null);
3265 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003266 clearChangeListeners();
3267 mTo.append(to);
3268 initChangeListeners();
3269 }
3270 }
3271 } else {
3272 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003273 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003274 break;
Alice Yanga990a712013-03-13 18:37:00 -07003275 case REFERENCE_MESSAGE_LOADER:
3276 // Only populate mRefMessage and leave other fields untouched.
3277 if (data != null && data.moveToFirst()) {
3278 mRefMessage = new Message(data);
3279 }
Andy Huang9f855d62013-05-30 17:15:03 -07003280 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
Alice Yanga990a712013-03-13 18:37:00 -07003281 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003282 case LOADER_ACCOUNT_CURSOR:
3283 if (data != null && data.moveToFirst()) {
3284 // there are accounts now!
3285 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003286 final ArrayList<Account> accounts = new ArrayList<Account>();
3287 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003288 do {
3289 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003290 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003291 initializedAccounts.add(account);
3292 }
3293 accounts.add(account);
3294 } while (data.moveToNext());
3295 if (initializedAccounts.size() > 0) {
3296 findViewById(R.id.wait).setVisibility(View.GONE);
3297 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3298 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003299 mAccounts = initializedAccounts.toArray(
3300 new Account[initializedAccounts.size()]);
3301
Mindy Pereirab199d172012-08-13 11:04:03 -07003302 finishCreate();
3303 invalidateOptionsMenu();
3304 } else {
3305 // Show "waiting"
3306 account = accounts.size() > 0 ? accounts.get(0) : null;
3307 showWaitFragment(account);
3308 }
3309 }
3310 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003311 }
3312 }
3313
Mindy Pereirab199d172012-08-13 11:04:03 -07003314 private void showWaitFragment(Account account) {
3315 WaitFragment fragment = getWaitFragment();
3316 if (fragment != null) {
3317 fragment.updateAccount(account);
3318 } else {
3319 findViewById(R.id.wait).setVisibility(View.VISIBLE);
3320 replaceFragment(WaitFragment.newInstance(account, true),
3321 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3322 }
3323 }
3324
3325 private WaitFragment getWaitFragment() {
3326 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3327 }
3328
3329 private int replaceFragment(Fragment fragment, int transition, String tag) {
3330 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003331 fragmentTransaction.setTransition(transition);
3332 fragmentTransaction.replace(R.id.wait, fragment, tag);
3333 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3334 return transactionId;
3335 }
3336
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003337 @Override
3338 public void onLoaderReset(Loader<Cursor> arg0) {
3339 // Do nothing.
3340 }
Paul Westbrook83e6b572013-02-05 16:22:42 -08003341
3342 @Override
3343 public Context getActivityContext() {
3344 return this;
3345 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003346}