blob: 58240cf6f2c35d6353974aea69a0b4bad728a400 [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;
Mindy Pereirab199d172012-08-13 11:04:03 -070025import android.app.Fragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070026import android.app.FragmentTransaction;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070027import android.app.LoaderManager;
Mindy Pereira6349a042012-01-04 11:25:01 -080028import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080029import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080030import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070031import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080032import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080033import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070034import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080035import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080036import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080037import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080038import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.os.Handler;
40import android.os.HandlerThread;
Paul Westbrookf97588b2012-03-20 11:11:37 -070041import android.os.Parcelable;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070042import android.os.ParcelFileDescriptor;
Mindy Pereira82cc5662012-01-09 17:29:30 -080043import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080044import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080045import android.text.Html;
mindyped9c2f02012-10-12 10:02:08 -070046import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080047import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080048import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080050import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080051import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070052import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070053import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080054import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080055import android.view.Menu;
56import android.view.MenuInflater;
57import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080058import android.view.View;
59import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070060import android.view.ViewGroup;
mindyp62d3ec72012-08-24 13:04:09 -070061import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080062import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080063import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070064import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080065import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080066import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080067
Mindy Pereirac17d0732011-12-29 10:46:19 -080068import com.android.common.Rfc822Validator;
Andy Huang5c5fd572012-04-08 18:19:29 -070069import com.android.ex.chips.RecipientEditTextView;
70import com.android.mail.R;
mindyp40882432012-09-06 11:07:40 -070071import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080072import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080073import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080074import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080075import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080076import com.android.mail.providers.Address;
77import com.android.mail.providers.Attachment;
Mindy Pereira47d0e652012-07-23 09:45:07 -070078import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080079import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080080import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070081import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080082import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080083import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070084import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070085import com.android.mail.providers.UIProvider.DraftType;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -070086import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -070087import com.android.mail.ui.WaitFragment;
Mark Wei62066e42012-09-13 12:07:02 -070088import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Paul Westbrook92227f62012-03-20 10:32:51 -070089import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -070090import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -080091import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -070092import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -080093import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080094import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080095import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080096import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080097import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080098
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070099import java.io.FileNotFoundException;
100import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700101import java.io.UnsupportedEncodingException;
102import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800103import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700104import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800105import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800106import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800107import java.util.HashSet;
108import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700109import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700110import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800111import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800112
113public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800114 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
mindyp40882432012-09-06 11:07:40 -0700115 AttachmentAddedOrDeletedListener, OnAccountChangedListener, LoaderManager.LoaderCallbacks<Cursor>,
mindyp62d3ec72012-08-24 13:04:09 -0700116 TextView.OnEditorActionListener {
Mindy Pereira6349a042012-01-04 11:25:01 -0800117 // Identifiers for which type of composition this is
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700118 static final int COMPOSE = -1;
119 static final int REPLY = 0;
120 static final int REPLY_ALL = 1;
121 static final int FORWARD = 2;
122 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800123
124 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700125 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800126
Mindy Pereira326689d2012-05-17 10:14:14 -0700127 private static final String EXTRA_SHOW_CC = "showCc";
128 private static final String EXTRA_SHOW_BCC = "showBcc";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700129
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700130 private static final String UTF8_ENCODING_NAME = "UTF-8";
131
132 private static final String MAIL_TO = "mailto";
133
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700134 private static final String EXTRA_SUBJECT = "subject";
135
136 private static final String EXTRA_BODY = "body";
137
mindypd27b6ea2012-10-05 09:43:49 -0700138 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700139
Mark Wei62066e42012-09-13 12:07:02 -0700140 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
141
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700142 // Extra that we can get passed from other activities
143 private static final String EXTRA_TO = "to";
144 private static final String EXTRA_CC = "cc";
145 private static final String EXTRA_BCC = "bcc";
146
147 // List of all the fields
148 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
149
Mindy Pereira82cc5662012-01-09 17:29:30 -0800150 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
151 // Map containing information about requests to create new messages, and the id of the
152 // messages that were the result of those requests.
153 //
154 // This map is used when the activity that initiated the save a of a new message, is killed
155 // before the save has completed (and when we know the id of the newly created message). When
156 // a save is completed, the service that is running in the background, will update the map
157 //
158 // When a new ComposeActivity instance is created, it will attempt to use the information in
159 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
160 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
161 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700162 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800163 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
164 // Key used to store the above map
165 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800166 /**
167 * Notifies the {@code Activity} that the caller is an Email
168 * {@code Activity}, so that the back behavior may be modified accordingly.
169 *
170 * @see #onAppUpPressed
171 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700172 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800173
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700174 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700175
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800176 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700177 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700178 // If this is a reply/forward then this extra will hold a uri we must query
179 // to get the original message.
180 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700181 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700182 // draft message
183 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800184 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700185 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800186 // Request numbers for activities we start
187 private static final int RESULT_PICK_ATTACHMENT = 1;
188 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700189 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700190 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700191
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700192 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
193 private static final String EXTRA_REQUEST_ID = "requestId";
194 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
195 private static final String EXTRA_FOCUS_SELECTION_END = null;
196 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700197 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700198 private static final int LOADER_ACCOUNT_CURSOR = 1;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700199 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700200 private static final String TAG_WAIT = "wait-fragment";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700201 private static final String MIME_TYPE_PHOTO = "image/*";
202 private static final String MIME_TYPE_VIDEO = "video/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800203
Mindy Pereira82cc5662012-01-09 17:29:30 -0800204 /**
205 * A single thread for running tasks in the background.
206 */
207 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800208 private RecipientEditTextView mTo;
209 private RecipientEditTextView mCc;
210 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800211 private Button mCcBccButton;
212 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800213 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800214 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700215 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800216 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800217 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800218 private TextView mSubject;
219
Mindy Pereira326c6602012-01-04 15:32:42 -0800220 private ComposeModeAdapter mComposeModeAdapter;
221 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800222 private boolean mForward;
223 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800224 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700225 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800226 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800227 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800228 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700229 @VisibleForTesting
230 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800231 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800232 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800233 private boolean mTextChanged;
234 private boolean mReplyFromChanged;
235 private MenuItem mSave;
236 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800237 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800238 private AlertDialog mSendConfirmDialog;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700239 @VisibleForTesting
240 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800241 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
242 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800243 private ReplyFromAccount mDraftAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800244 private Object mDraftLock = new Object();
mindyp93b079b2012-08-29 16:32:15 -0700245 private View mPhotoAttachmentsButton;
246 private View mVideoAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800247
Mindy Pereira326c6602012-01-04 15:32:42 -0800248 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700249 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
250 */
251 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700252 private RecipientTextWatcher mToListener;
253 private RecipientTextWatcher mCcListener;
254 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700255 private Uri mRefMessageUri;
Mindy Pereirab199d172012-08-13 11:04:03 -0700256 private Bundle mSavedInstanceState;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700257
258
259 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800260 * Can be called from a non-UI thread.
261 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800262 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700263 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800264 }
265
Mindy Pereira6349a042012-01-04 11:25:01 -0800266 /**
267 * Can be called from a non-UI thread.
268 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800269 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800270 launch(launcher, account, null, COMPOSE);
271 }
272
273 /**
274 * Can be called from a non-UI thread.
275 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800276 public static void reply(Context launcher, Account account, Message message) {
277 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800278 }
279
280 /**
281 * Can be called from a non-UI thread.
282 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800283 public static void replyAll(Context launcher, Account account, Message message) {
284 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800285 }
286
287 /**
288 * Can be called from a non-UI thread.
289 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800290 public static void forward(Context launcher, Account account, Message message) {
291 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800292 }
293
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800294 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800295 Intent intent = new Intent(launcher, ComposeActivity.class);
296 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
297 intent.putExtra(EXTRA_ACTION, action);
298 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700299 if (action == EDIT_DRAFT) {
300 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
301 } else {
302 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
303 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800304 launcher.startActivity(intent);
305 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800306
307 @Override
308 public void onCreate(Bundle savedInstanceState) {
309 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800310 setContentView(R.layout.compose);
Mindy Pereirab199d172012-08-13 11:04:03 -0700311 mSavedInstanceState = savedInstanceState;
312 checkValidAccounts();
313 }
314
315 private void finishCreate() {
316 Bundle savedInstanceState = mSavedInstanceState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800317 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800318 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700319 Message message;
Mark Wei62066e42012-09-13 12:07:02 -0700320 ArrayList<AttachmentPreview> previews;
Mindy Pereira71c9e562012-05-17 11:01:02 -0700321 boolean showQuotedText = false;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700322 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700323 // Check for any of the possibly supplied accounts.;
324 Account account = null;
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700325 if (hadSavedInstanceStateMessage(savedInstanceState)) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700326 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
327 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
328 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700329
330 previews = savedInstanceState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700331 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
332 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700333 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700334 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
335 // Initialize the message from the message in the intent
336 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700337 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700338 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700339 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700340 }
Mark Wei62066e42012-09-13 12:07:02 -0700341 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700342
343 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800344 if (mAccount == null) {
345 return;
346 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700347
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700348 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
349 mLaunchedFromEmail = true;
350 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
351 final Uri dataUri = intent.getData();
352 if (dataUri != null) {
353 final String dataScheme = intent.getData().getScheme();
354 final String accountScheme = mAccount.composeIntentUri.getScheme();
355 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
356 }
357 }
358
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700359 if (mRefMessageUri != null) {
360 // We have a referenced message that we must look up.
361 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
362 return;
363 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700364 initFromDraftMessage(message);
365 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700366 showCcBcc(savedInstanceState);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700367 showQuotedText = message.appendRefMessageContent;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700368 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700369 initFromDraftMessage(message);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800370 boolean showBcc = !TextUtils.isEmpty(message.getBcc());
371 boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
Mindy Pereiraef388302012-06-18 19:07:44 -0700372 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700373 // Update the action to the draft type of the previous draft
374 switch (message.draftType) {
375 case UIProvider.DraftType.REPLY:
376 action = REPLY;
377 break;
378 case UIProvider.DraftType.REPLY_ALL:
379 action = REPLY_ALL;
380 break;
381 case UIProvider.DraftType.FORWARD:
382 action = FORWARD;
383 break;
384 case UIProvider.DraftType.COMPOSE:
385 default:
386 action = COMPOSE;
387 break;
388 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700389 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700390 showQuotedText = message.appendRefMessageContent;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700391 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
392 if (mRefMessage != null) {
393 initFromRefMessage(action, mAccount.name);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700394 showQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700395 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700396 } else {
397 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700398 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700399 finishSetup(action, intent, savedInstanceState, showQuotedText);
400 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700401
Mindy Pereirab199d172012-08-13 11:04:03 -0700402 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700403 final Account[] allAccounts = AccountUtils.getAccounts(this);
404 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700405 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
406 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700407 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700408 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
409 }
410 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700411 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700412 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700413 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700414 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700415 anySyncing = true;
416 break;
417 }
418 }
419 if (!anySyncing) {
420 // There are accounts, but none are sync'd, which is just like having no accounts.
421 mAccounts = null;
422 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
423 return;
424 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700425 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700426 finishCreate();
427 }
428 }
429
Mindy Pereira47d0e652012-07-23 09:45:07 -0700430 private Account obtainAccount(Intent intent) {
431 Account account = null;
432 Object accountExtra = null;
433 if (intent != null && intent.getExtras() != null) {
434 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
435 if (accountExtra instanceof Account) {
436 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700437 } else if (accountExtra instanceof String) {
438 // This is the Account attached to the widget compose intent.
439 account = Account.newinstance((String)accountExtra);
440 if (account != null) {
441 return account;
442 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700443 }
444 accountExtra = intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
445 }
446 if (account == null) {
mindyp06174462012-10-12 09:11:27 -0700447 MailAppProvider provider = MailAppProvider.getInstance();
448 String lastAccountUri = provider.getLastSentFromAccount();
449 if (TextUtils.isEmpty(lastAccountUri)) {
450 lastAccountUri = provider.getLastViewedAccount();
451 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700452 if (!TextUtils.isEmpty(lastAccountUri)) {
453 accountExtra = Uri.parse(lastAccountUri);
454 }
455 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700456 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700457 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
458 // For backwards compatibility, we need to check account
459 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700460 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700461 if (a.name.equals(accountExtra)) {
462 account = a;
463 }
464 }
465 } else if (accountExtra instanceof Uri) {
466 // The uri of the last viewed account is what is stored in
467 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700468 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700469 if (a.uri.equals(accountExtra)) {
470 account = a;
471 }
472 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700473 }
474 if (account == null) {
475 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700476 }
477 }
478 return account;
479 }
480
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700481 private void finishSetup(int action, Intent intent, Bundle savedInstanceState,
482 boolean showQuotedText) {
mindyp34a3c562012-11-06 15:12:15 -0800483 setFocus(action);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700484 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800485 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800486 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800487 initRecipients();
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700488 // Don't bother with the intent if we have procured a message from the
489 // intent already.
490 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
491 initAttachmentsFromIntent(intent);
492 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800493 initActionBar(action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700494 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
495 action);
mindypd4a48662012-11-08 17:13:49 -0800496
497 // If this is a draft message, the draft account is whatever account was
498 // used to open the draft message in Compose.
499 if (mDraft != null) {
500 mDraftAccount = mReplyFromAccount;
501 }
502
Mindy Pereira75f66632012-01-11 11:42:02 -0800503 initChangeListeners();
Mindy Pereira326689d2012-05-17 10:14:14 -0700504 updateHideOrShowCcBcc();
Mindy Pereira71c9e562012-05-17 11:01:02 -0700505 updateHideOrShowQuotedText(showQuotedText);
506 }
507
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700508 private boolean hadSavedInstanceStateMessage(Bundle savedInstanceState) {
509 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
510 }
511
Mindy Pereira71c9e562012-05-17 11:01:02 -0700512 private void updateHideOrShowQuotedText(boolean showQuotedText) {
513 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700514 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700515 }
516
517 private void setFocus(int action) {
518 if (action == EDIT_DRAFT) {
519 int type = mDraft.draftType;
520 switch (type) {
521 case UIProvider.DraftType.COMPOSE:
522 case UIProvider.DraftType.FORWARD:
523 action = COMPOSE;
524 break;
525 case UIProvider.DraftType.REPLY:
526 case UIProvider.DraftType.REPLY_ALL:
527 default:
528 action = REPLY;
529 break;
530 }
531 }
532 switch (action) {
533 case FORWARD:
534 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800535 if (TextUtils.isEmpty(mTo.getText())) {
536 mTo.requestFocus();
537 break;
538 }
Mindy Pereira433b1982012-04-03 11:53:07 -0700539 case REPLY:
540 case REPLY_ALL:
541 default:
542 focusBody();
543 break;
544 }
545 }
546
547 /**
548 * Focus the body of the message.
549 */
550 public void focusBody() {
551 mBodyView.requestFocus();
552 int length = mBodyView.getText().length();
553
554 int signatureStartPos = getSignatureStartPosition(
555 mSignature, mBodyView.getText().toString());
556 if (signatureStartPos > -1) {
557 // In case the user deleted the newlines...
558 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700559 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700560 // Move cursor to the end.
561 mBodyView.setSelection(length);
562 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800563 }
564
565 @Override
566 protected void onResume() {
567 super.onResume();
568 // Update the from spinner as other accounts
569 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800570 if (mFromSpinner != null && mAccount != null) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700571 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount, mAccounts);
Mindy Pereira818143e2012-01-11 13:59:49 -0800572 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800573 }
574
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800575 @Override
576 protected void onPause() {
577 super.onPause();
578
579 if (mSendConfirmDialog != null) {
580 mSendConfirmDialog.dismiss();
581 }
582 if (mRecipientErrorDialog != null) {
583 mRecipientErrorDialog.dismiss();
584 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700585 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700586 // Don't save unnecessary drafts if we are only changing the orientation.
587 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700588 saveIfNeeded();
589 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800590 }
591
592 @Override
593 protected final void onActivityResult(int request, int result, Intent data) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700594 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800595 addAttachmentAndUpdateView(data);
Mindy Pereirab199d172012-08-13 11:04:03 -0700596 mAddingAttachment = false;
597 } else if (request == RESULT_CREATE_ACCOUNT) {
598 // We were waiting for the user to create an account
599 if (result != RESULT_OK) {
600 finish();
601 } else {
602 // Watch for accounts to show up!
603 // restart the loader to get the updated list of accounts
604 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
605 showWaitFragment(null);
606 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800607 }
608 }
609
610 @Override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700611 public final void onRestoreInstanceState(Bundle savedInstanceState) {
mindyp47db6d72012-11-14 09:58:28 -0800612 clearChangeListeners();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700613 super.onRestoreInstanceState(savedInstanceState);
614 if (savedInstanceState != null) {
615 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
616 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
617 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
618 // There should be a focus and it should be an EditText since we
619 // only save these extras if these conditions are true.
620 EditText focusEditText = (EditText) getCurrentFocus();
621 final int length = focusEditText.getText().length();
622 if (selectionStart < length && selectionEnd < length) {
623 focusEditText.setSelection(selectionStart, selectionEnd);
624 }
625 }
626 }
627 }
628
629 @Override
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800630 public final void onSaveInstanceState(Bundle state) {
631 super.onSaveInstanceState(state);
Mindy Pereirab199d172012-08-13 11:04:03 -0700632 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
633 if (mAccounts == null || mAccounts.length == 0) {
634 return;
635 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700636 // The framework is happy to save and restore the selection but only if it also saves and
637 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
638 // this manually.
639 View focus = getCurrentFocus();
640 if (focus != null && focus instanceof EditText) {
641 EditText focusEditText = (EditText) focus;
642 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
643 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
644 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700645
646 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700647 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700648 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
649 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
650 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700651 if (selectedReplyFromAccount != null) {
652 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
653 .toString());
654 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
655 } else {
656 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
657 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800658
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700659 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
660 // We don't have a draft id, and we have a request id,
661 // save the request id.
662 state.putInt(EXTRA_REQUEST_ID, mRequestId);
663 }
664
665 // We want to restore the current mode after a pause
666 // or rotation.
667 int mode = getMode();
668 state.putInt(EXTRA_ACTION, mode);
669
mindype7b76aa2012-11-14 16:19:13 -0800670 final Message message = createMessage(selectedReplyFromAccount, mode);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700671 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -0800672 message.id = mDraft.id;
673 message.serverId = mDraft.serverId;
674 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700675 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700676 state.putParcelable(EXTRA_MESSAGE, message);
677
678 if (mRefMessage != null) {
679 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
680 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700681 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
682 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
Mark Wei62066e42012-09-13 12:07:02 -0700683
684 state.putParcelableArrayList(
685 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700686 }
687
688 private int getMode() {
689 int mode = ComposeActivity.COMPOSE;
690 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700691 if (actionBar != null
692 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700693 mode = actionBar.getSelectedNavigationIndex();
694 }
695 return mode;
696 }
697
698 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
699 Message message = new Message();
700 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -0700701 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700702 message.uri = null;
703 message.conversationUri = null;
704 message.subject = mSubject.getText().toString();
705 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800706 message.setTo(formatSenders(mTo.getText().toString()));
707 message.setCc(formatSenders(mCc.getText().toString()));
708 message.setBcc(formatSenders(mBcc.getText().toString()));
709 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700710 message.dateReceivedMs = 0;
mindyped9c2f02012-10-12 10:02:08 -0700711 String htmlBody = Html.toHtml(new SpannableString(mBodyView.getText().toString()));
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700712 StringBuilder fullBody = new StringBuilder(htmlBody);
713 message.bodyHtml = fullBody.toString();
714 message.bodyText = mBodyView.getText().toString();
715 message.embedsExternalResources = false;
716 message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700717 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
718 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
719 message.hasAttachments = attachments != null && attachments.size() > 0;
720 message.attachmentListUri = null;
721 message.messageFlags = 0;
722 message.saveUri = null;
723 message.sendUri = null;
724 message.alwaysShowImages = false;
725 message.attachmentsJson = Attachment.toJSONArray(attachments);
726 CharSequence quotedText = mQuotedTextView.getQuotedText();
727 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
728 .getQuotedTextOffset(quotedText.toString()) : -1;
729 message.accountUri = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800730 message.setFrom(selectedReplyFromAccount != null ? selectedReplyFromAccount.address
731 : mAccount != null ? mAccount.name : null);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700732 message.draftType = getDraftType(mode);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800733 message.setTo(formatSenders(mTo.getText().toString()));
734 message.setCc(formatSenders(mCc.getText().toString()));
735 message.setBcc(formatSenders(mBcc.getText().toString()));
mindype7b76aa2012-11-14 16:19:13 -0800736 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700737 }
738
Mindy Pereira3c911582012-08-09 16:59:09 -0700739 private String formatSenders(String string) {
740 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
741 return string.substring(0, string.length() - 1);
742 }
743 return string;
744 }
745
Mindy Pereira818143e2012-01-11 13:59:49 -0800746 @VisibleForTesting
747 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700748 if (account == null) {
749 return;
750 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700751 if (!account.equals(mAccount)) {
752 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700753 mCachedSettings = mAccount.settings;
754 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700755 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700756 if (mAccount != null) {
757 MailActivity.setForegroundNdef(MailActivity.getMailtoNdef(mAccount.name));
758 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800759 }
760
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700761 private void initFromSpinner(Bundle bundle, int action) {
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700762 String accountString = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700763 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700764 action = COMPOSE;
765 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700766 mFromSpinner.asyncInitFromSpinner(action, mAccount, mAccounts);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700767 if (bundle != null) {
768 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
769 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
770 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
771 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
772 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
773 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
774 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700775 }
776 if (mReplyFromAccount == null) {
777 if (mDraft != null) {
778 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
779 } else if (mRefMessage != null) {
780 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
781 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700782 }
783 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -0700784 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700785 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700786
Mindy Pereira62de1b12012-04-06 12:17:56 -0700787 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700788
Mindy Pereira62de1b12012-04-06 12:17:56 -0700789 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700790 // If there is only 1 account, just show that account.
791 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700792 // send mail from / save drafts to.
793 mFromStatic.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700794 mFromStaticText.setText(mAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700795 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700796 } else {
797 mFromStatic.setVisibility(View.VISIBLE);
798 mFromStaticText.setText(mAccount.name);
799 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700800 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800801 }
802
Mindy Pereira62de1b12012-04-06 12:17:56 -0700803 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
804 if (refMessage.accountUri != null) {
805 // This must be from combined inbox.
806 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
807 for (ReplyFromAccount from : replyFromAccounts) {
808 if (from.account.uri.equals(refMessage.accountUri)) {
809 return from;
810 }
811 }
812 return null;
813 } else {
814 return getReplyFromAccount(account, refMessage);
815 }
816 }
817
818 /**
819 * Given an account and which email address the message was sent to,
820 * return who the message should be sent from.
821 * @param account Account in which the message arrived.
822 * @param sentTo Email address to which the message was sent.
823 * @return the address from which to reply.
824 */
825 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
826 // First see if we are supposed to use the default address or
827 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -0700828 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700829 return getDefaultReplyFromAccount(account);
830 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -0700831 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -0700832 // all the message recipients and find one that matches
833 // a custom from or account.
834 List<String> allRecipients = new ArrayList<String>();
mindyp5ed63112012-09-17 17:31:45 -0700835 allRecipients.addAll(Arrays.asList(refMessage.getToAddresses()));
836 allRecipients.addAll(Arrays.asList(refMessage.getCcAddresses()));
Mindy Pereira62de1b12012-04-06 12:17:56 -0700837 return getMatchingRecipient(account, allRecipients);
838 }
839 }
840
841 /**
842 * Compare all the recipients of an email to the current account and all
843 * custom addresses associated with that account. Return the match if there
844 * is one, or the default account if there isn't.
845 */
846 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
847 // Tokenize the list and place in a hashmap.
848 ReplyFromAccount matchingReplyFrom = null;
849 Rfc822Token[] tokens;
850 HashSet<String> recipientsMap = new HashSet<String>();
851 for (String address : sentTo) {
852 tokens = Rfc822Tokenizer.tokenize(address);
853 for (int i = 0; i < tokens.length; i++) {
854 recipientsMap.add(tokens[i].getAddress());
855 }
856 }
857
858 int matchingAddressCount = 0;
859 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -0700860 customFroms = account.getReplyFroms();
861 if (customFroms != null) {
862 for (ReplyFromAccount entry : customFroms) {
863 if (recipientsMap.contains(entry.address)) {
864 matchingReplyFrom = entry;
865 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -0700866 }
867 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700868 }
869 if (matchingAddressCount > 1) {
870 matchingReplyFrom = getDefaultReplyFromAccount(account);
871 }
872 return matchingReplyFrom;
873 }
874
875 private ReplyFromAccount getDefaultReplyFromAccount(Account account) {
Andy Huang238aa472012-10-30 17:45:17 -0700876 for (ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700877 if (from.isDefault) {
878 return from;
879 }
880 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700881 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
882 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700883 }
884
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700885 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800886 String sender = msg.getFrom();
Mindy Pereira62de1b12012-04-06 12:17:56 -0700887 ReplyFromAccount replyFromAccount = null;
888 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
889 if (TextUtils.equals(account.name, sender)) {
890 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700891 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700892 } else {
893 for (ReplyFromAccount fromAccount : replyFromAccounts) {
894 if (TextUtils.equals(fromAccount.name, sender)) {
895 replyFromAccount = fromAccount;
896 break;
897 }
898 }
899 }
900 return replyFromAccount;
901 }
902
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800903 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -0700904 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800905 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800906 if (mCcBccButton != null) {
907 mCcBccButton.setOnClickListener(this);
908 }
909 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800910 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
mindyp93b079b2012-08-29 16:32:15 -0700911 mPhotoAttachmentsButton = findViewById(R.id.add_photo_attachment);
mindypcd0b0b92012-08-23 14:33:17 -0700912 if (mPhotoAttachmentsButton != null) {
913 mPhotoAttachmentsButton.setOnClickListener(this);
914 }
mindyp93b079b2012-08-29 16:32:15 -0700915 mVideoAttachmentsButton = findViewById(R.id.add_video_attachment);
mindypcd0b0b92012-08-23 14:33:17 -0700916 if (mVideoAttachmentsButton != null) {
917 mVideoAttachmentsButton.setOnClickListener(this);
918 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800919 mTo = (RecipientEditTextView) findViewById(R.id.to);
920 mCc = (RecipientEditTextView) findViewById(R.id.cc);
921 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800922 // TODO: add special chips text change watchers before adding
923 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800924 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -0700925 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800926 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
927 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -0700928 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800929 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800930 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800931 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800932 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800933 }
934
mindyp62d3ec72012-08-24 13:04:09 -0700935 @Override
936 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
937 if (action == EditorInfo.IME_ACTION_DONE) {
938 focusBody();
939 return true;
940 }
941 return false;
942 }
943
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700944 protected TextView getBody() {
945 return mBodyView;
946 }
947
948 @VisibleForTesting
949 public Account getFromAccount() {
950 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
951 mReplyFromAccount.account : mAccount;
952 }
953
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700954 private void clearChangeListeners() {
955 mSubject.removeTextChangedListener(this);
956 mBodyView.removeTextChangedListener(this);
957 mTo.removeTextChangedListener(mToListener);
958 mCc.removeTextChangedListener(mCcListener);
959 mBcc.removeTextChangedListener(mBccListener);
960 mFromSpinner.setOnAccountChangedListener(null);
961 mAttachmentsView.setAttachmentChangesListener(null);
962 }
963
Mindy Pereira75f66632012-01-11 11:42:02 -0800964 // Now that the message has been initialized from any existing draft or
965 // ref message data, set up listeners for any changes that occur to the
966 // message.
967 private void initChangeListeners() {
968 mSubject.addTextChangedListener(this);
969 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700970 if (mToListener == null) {
971 mToListener = new RecipientTextWatcher(mTo, this);
972 }
973 mTo.addTextChangedListener(mToListener);
974 if (mCcListener == null) {
975 mCcListener = new RecipientTextWatcher(mCc, this);
976 }
977 mCc.addTextChangedListener(mCcListener);
978 if (mBccListener == null) {
979 mBccListener = new RecipientTextWatcher(mBcc, this);
980 }
981 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -0800982 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800983 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800984 }
985
Mindy Pereira326c6602012-01-04 15:32:42 -0800986 private void initActionBar(int action) {
987 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800988 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700989 if (actionBar == null) {
990 return;
991 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800992 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800993 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
994 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800995 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800996 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800997 if (mComposeModeAdapter == null) {
998 mComposeModeAdapter = new ComposeModeAdapter(this);
999 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001000 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1001 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -08001002 switch (action) {
1003 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001004 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001005 break;
1006 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001007 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001008 break;
1009 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001010 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001011 break;
1012 }
1013 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001014 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1015 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1016 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001017 }
1018
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001019 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001020 setFieldsFromRefMessage(action, recipientAddress);
1021 if (mRefMessage != null) {
1022 // CC field only gets populated when doing REPLY_ALL.
1023 // BCC never gets auto-populated, unless the user is editing
1024 // a draft with one.
Mindy Pereira29a717e2012-07-25 18:05:48 -07001025 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001026 mCcBccView.show(false, true, false);
1027 }
1028 }
1029 updateHideOrShowCcBcc();
1030 }
1031
1032 private void setFieldsFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001033 setSubject(mRefMessage, action);
1034 // Setup recipients
1035 if (action == FORWARD) {
1036 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001037 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001038 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001039 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001040 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1041 initAttachments(mRefMessage);
1042 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001043 }
1044
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001045 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001046 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001047
1048 mDraft = message;
1049 mDraftId = message.id;
1050 mSubject.setText(message.subject);
1051 mForward = message.draftType == UIProvider.DraftType.FORWARD;
1052 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
1053 addToAddresses(toAddresses);
1054 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
1055 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001056 if (message.hasAttachments) {
1057 List<Attachment> attachments = message.getAttachments();
1058 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001059 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001060 }
1061 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -07001062 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -07001063 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001064 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001065 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001066 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001067 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001068 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001069 // Find the offset in the htmltext of the actual quoted text and strip it out.
1070 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1071 if (quotedTextIndex > -1) {
mindypc59dd822012-11-13 10:56:21 -08001072 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1073 quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001074 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1075 message.bodyHtml.length());
1076 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001077 } else {
mindypc59dd822012-11-13 10:56:21 -08001078 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001079 }
1080 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001081 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001082 final String body = message.bodyText;
1083 final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1084 (quotedTextIndex > -1 ?
1085 message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1086 : "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001087 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001088 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1089 : null;
Mindy Pereira002ff522012-05-30 10:31:26 -07001090 }
1091 mBodyView.setText(bodyText);
1092 }
1093 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001094 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001095 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001096 }
1097
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001098 /**
1099 * Fill all the widgets with the content found in the Intent Extra, if any.
1100 * Also apply the same style to all widgets. Note: if initFromExtras is
1101 * called as a result of switching between reply, reply all, and forward per
1102 * the latest revision of Gmail, and the user has already made changes to
1103 * attachments on a previous incarnation of the message (as a reply, reply
1104 * all, or forward), the original attachments from the message will not be
1105 * re-instantiated. The user's changes will be respected. This follows the
1106 * web gmail interaction.
1107 */
1108 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001109 // If we were invoked with a SENDTO intent, the value
1110 // should take precedence
1111 final Uri dataUri = intent.getData();
1112 if (dataUri != null) {
1113 if (MAIL_TO.equals(dataUri.getScheme())) {
1114 initFromMailTo(dataUri.toString());
1115 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001116 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001117 String toText = dataUri.getSchemeSpecificPart();
1118 if (toText != null) {
1119 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001120 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001121 }
1122 }
1123 }
1124 }
1125
1126 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1127 if (extraStrings != null) {
1128 addToAddresses(Arrays.asList(extraStrings));
1129 }
1130 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1131 if (extraStrings != null) {
1132 addCcAddresses(Arrays.asList(extraStrings), null);
1133 }
1134 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1135 if (extraStrings != null) {
1136 addBccAddresses(Arrays.asList(extraStrings));
1137 }
1138
1139 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1140 if (extraString != null) {
1141 mSubject.setText(extraString);
1142 }
1143
1144 for (String extra : ALL_EXTRAS) {
1145 if (intent.hasExtra(extra)) {
1146 String value = intent.getStringExtra(extra);
1147 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001148 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001149 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001150 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001151 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001152 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001153 } else if (EXTRA_SUBJECT.equals(extra)) {
1154 mSubject.setText(value);
1155 } else if (EXTRA_BODY.equals(extra)) {
1156 setBody(value, true /* with signature */);
1157 }
1158 }
1159 }
1160
1161 Bundle extras = intent.getExtras();
1162 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001163 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1164 if (text != null) {
1165 setBody(text, true /* with signature */);
1166 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001167 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001168 }
1169
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001170 @VisibleForTesting
1171 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001172 // TODO: handle the case where there are spaces in the display name as
1173 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1174 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001175 // Since URLDecode.decode changes + into ' ', and + is a valid
1176 // email character, we need to find/ replace these ourselves before
1177 // decoding.
1178 String replacePlus = s.replace("+", "%2B");
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001179 try {
1180 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
1181 } catch (IllegalArgumentException e) {
1182 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1183 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1184 } else {
1185 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1186 }
1187 return null;
1188 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001189 }
1190
1191 /**
1192 * Initialize the compose view from a String representing a mailTo uri.
1193 * @param mailToString The uri as a string.
1194 */
1195 public void initFromMailTo(String mailToString) {
1196 // We need to disguise this string as a URI in order to parse it
1197 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1198 Uri uri = Uri.parse("foo://" + mailToString);
1199 int index = mailToString.indexOf("?");
1200 int length = "mailto".length() + 1;
1201 String to;
1202 try {
1203 // Extract the recipient after mailto:
1204 if (index == -1) {
1205 to = decodeEmailInUri(mailToString.substring(length));
1206 } else {
1207 to = decodeEmailInUri(mailToString.substring(length, index));
1208 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001209 if (!TextUtils.isEmpty(to)) {
1210 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1211 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001212 } catch (UnsupportedEncodingException e) {
1213 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1214 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1215 } else {
1216 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1217 }
1218 }
1219
1220 List<String> cc = uri.getQueryParameters("cc");
1221 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1222
1223 List<String> otherTo = uri.getQueryParameters("to");
1224 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1225
1226 List<String> bcc = uri.getQueryParameters("bcc");
1227 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1228
1229 List<String> subject = uri.getQueryParameters("subject");
1230 if (subject.size() > 0) {
1231 try {
1232 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
1233 } catch (UnsupportedEncodingException e) {
1234 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1235 e.getMessage(), subject);
1236 }
1237 }
1238
1239 List<String> body = uri.getQueryParameters("body");
1240 if (body.size() > 0) {
1241 try {
1242 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
1243 true /* with signature */);
1244 } catch (UnsupportedEncodingException e) {
1245 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1246 }
1247 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001248 }
1249
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001250 @VisibleForTesting
1251 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001252 addAttachments(refMessage.getAttachments());
1253 }
1254
1255 public long addAttachments(List<Attachment> attachments) {
1256 long size = 0;
1257 AttachmentFailureException error = null;
1258 for (Attachment a : attachments) {
1259 try {
1260 size += mAttachmentsView.addAttachment(mAccount, a);
1261 } catch (AttachmentFailureException e) {
1262 error = e;
1263 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001264 }
Mark Wei434f2942012-08-24 11:54:02 -07001265 if (error != null) {
1266 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1267 if (attachments.size() > 1) {
1268 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1269 } else {
1270 showAttachmentTooBigToast(error.getErrorRes());
1271 }
1272 }
1273 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001274 }
1275
1276 /**
1277 * When an attachment is too large to be added to a message, show a toast.
1278 * This method also updates the position of the toast so that it is shown
1279 * clearly above they keyboard if it happens to be open.
1280 */
Mark Wei434f2942012-08-24 11:54:02 -07001281 private void showAttachmentTooBigToast(int errorRes) {
1282 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1283 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1284 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001285 }
1286
Mark Wei434f2942012-08-24 11:54:02 -07001287 private void showErrorToast(String message) {
1288 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1289 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001290 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1291 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1292 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001293 }
1294
Paul Westbrookf97588b2012-03-20 11:11:37 -07001295 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001296 Bundle extras = intent.getExtras();
1297 if (extras == null) {
1298 extras = Bundle.EMPTY;
1299 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001300 final String action = intent.getAction();
1301 if (!mAttachmentsChanged) {
1302 long totalSize = 0;
1303 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1304 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1305 for (String uriString : uris) {
1306 final Uri uri = Uri.parse(uriString);
1307 long size = 0;
1308 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001309 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001310 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001311 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001312 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001313 }
1314 totalSize += size;
1315 }
1316 }
mindyp9a9e8d62012-10-03 12:24:07 -07001317 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1318 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1319 ArrayList<Parcelable> uris = extras
1320 .getParcelableArrayList(Intent.EXTRA_STREAM);
1321 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1322 for (Parcelable uri : uris) {
1323 try {
1324 attachments.add(mAttachmentsView.generateLocalAttachment((Uri) uri));
1325 } catch (AttachmentFailureException e) {
1326 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1327 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1328 getApplicationContext(),
1329 mAccount.settings.getMaxAttachmentSize());
1330 showErrorToast(getString
1331 (R.string.generic_attachment_problem, maxSize));
1332 }
1333 }
1334 totalSize += addAttachments(attachments);
1335 } else {
1336 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1337 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001338 try {
mindyp9a9e8d62012-10-03 12:24:07 -07001339 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001340 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001341 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001342 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001343 }
mindyp9a9e8d62012-10-03 12:24:07 -07001344 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001345 }
1346 }
1347
1348 if (totalSize > 0) {
1349 mAttachmentsChanged = true;
1350 updateSaveUi();
1351 }
1352 }
1353 }
1354
1355
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001356 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1357 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001358 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1359 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001360 }
1361
1362 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001363 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001364 boolean ccVisible = mCcBccView.isCcVisible();
1365 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001366 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001367 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001368 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001369 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001370 : R.string.add_bcc_label));
1371 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001372 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001373 }
1374 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001375 }
1376
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001377 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001378 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1379 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1380 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1381 if (showCc || showBcc) {
1382 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001383 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001384 }
1385 }
1386
Mindy Pereira013194c2012-01-06 15:09:33 -08001387 /**
1388 * Add attachment and update the compose area appropriately.
1389 * @param data
1390 */
1391 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001392 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1393 }
1394
Andy Huang5c5fd572012-04-08 18:19:29 -07001395 public void addAttachmentAndUpdateView(Uri contentUri) {
1396 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001397 return;
1398 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001399 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001400 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1401 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001402 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001403 showErrorToast(getResources().getString(
1404 e.getErrorRes(),
1405 AttachmentUtils.convertToHumanReadableSize(
1406 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001407 }
1408 }
1409
1410 public void addAttachmentAndUpdateView(Attachment attachment) {
1411 try {
Mark Wei434f2942012-08-24 11:54:02 -07001412 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001413 if (size > 0) {
1414 mAttachmentsChanged = true;
1415 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001416 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001417 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001418 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001419 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001420 }
1421 }
1422
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001423 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001424 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001425 // Don't populate the address if this is a forward.
1426 if (action == ComposeActivity.FORWARD) {
1427 return;
1428 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001429 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001430 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001431
Mindy Pereira818143e2012-01-11 13:59:49 -08001432 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001433 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001434 // This is the email address of the current user, i.e. the one composing
1435 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001436 final String accountEmail = Address.getEmailAddress(account).getAddress();
mindyp5ed63112012-09-17 17:31:45 -07001437 String[] sentToAddresses = refMessage.getToAddresses();
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001438 String replytoAddress = refMessage.getReplyTo();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001439 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001440
1441 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1442 // Cc list is the union of the To and Cc recipients of the original
1443 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001444 // already on the To list.
1445 if (action == ComposeActivity.REPLY) {
mindyp5ee5d692012-11-19 16:02:16 -08001446 toAddresses = initToRecipients(accountEmail, refMessage.getFrom(),
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001447 replytoAddress, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001448 addToAddresses(toAddresses);
1449 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001450 final Set<String> ccAddresses = Sets.newHashSet();
mindyp5ee5d692012-11-19 16:02:16 -08001451 toAddresses = initToRecipients(accountEmail, refMessage.getFrom(),
Scott Kennedy8960f0a2012-11-07 15:35:50 -08001452 replytoAddress, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001453 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001454 addRecipients(accountEmail, ccAddresses, sentToAddresses);
mindyp5ed63112012-09-17 17:31:45 -07001455 addRecipients(accountEmail, ccAddresses, refMessage.getCcAddresses());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001456 addCcAddresses(ccAddresses, toAddresses);
1457 }
1458 }
1459
1460 private void addToAddresses(Collection<String> addresses) {
1461 addAddressesToList(addresses, mTo);
1462 }
1463
1464 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001465 addCcAddressesToList(tokenizeAddressList(addresses),
1466 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001467 }
1468
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001469 private void addBccAddresses(Collection<String> addresses) {
1470 addAddressesToList(addresses, mBcc);
1471 }
1472
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001473 @VisibleForTesting
1474 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1475 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1476 String address;
1477
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001478 if (compareToList == null) {
1479 for (Rfc822Token[] tokens : addresses) {
1480 for (int i = 0; i < tokens.length; i++) {
1481 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001482 list.append(address + END_TOKEN);
1483 }
1484 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001485 } else {
1486 HashSet<String> compareTo = convertToHashSet(compareToList);
1487 for (Rfc822Token[] tokens : addresses) {
1488 for (int i = 0; i < tokens.length; i++) {
1489 address = tokens[i].toString();
1490 // Check if this is a duplicate:
1491 if (!compareTo.contains(tokens[i].getAddress())) {
1492 // Get the address here
1493 list.append(address + END_TOKEN);
1494 }
1495 }
1496 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001497 }
1498 }
1499
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001500 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
1501 HashSet<String> hash = new HashSet<String>();
1502 for (Rfc822Token[] tokens : list) {
1503 for (int i = 0; i < tokens.length; i++) {
1504 hash.add(tokens[i].getAddress());
1505 }
1506 }
1507 return hash;
1508 }
1509
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001510 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1511 @VisibleForTesting
1512 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1513
1514 for (String address: addresses) {
1515 tokenized.add(Rfc822Tokenizer.tokenize(address));
1516 }
1517 return tokenized;
1518 }
1519
1520 @VisibleForTesting
1521 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1522 for (String address : addresses) {
1523 addAddressToList(address, list);
1524 }
1525 }
1526
1527 private void addAddressToList(String address, RecipientEditTextView list) {
1528 if (address == null || list == null)
1529 return;
1530
1531 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1532
1533 for (int i = 0; i < tokens.length; i++) {
1534 list.append(tokens[i] + END_TOKEN);
1535 }
1536 }
1537
1538 @VisibleForTesting
mindyp5ee5d692012-11-19 16:02:16 -08001539 protected Collection<String> initToRecipients(String accountEmail,
mindyp89576472012-11-06 17:07:08 -08001540 String fullSenderAddress, String replyToAddress,
1541 String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001542 // The To recipient is the reply-to address specified in the original
1543 // message, unless it is:
1544 // the current user OR a custom from of the current user, in which case
1545 // it's the To recipient list of the original message.
1546 // OR missing, in which case use the sender of the original message
1547 Set<String> toAddresses = Sets.newHashSet();
mindypfe8557b2012-11-05 12:05:16 -08001548 if (!TextUtils.isEmpty(replyToAddress)
mindyp5ee5d692012-11-19 16:02:16 -08001549 && !recipientMatchesThisAccount(replyToAddress)) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001550 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001551 } else {
mindyp5ee5d692012-11-19 16:02:16 -08001552 if (!recipientMatchesThisAccount(fullSenderAddress)) {
mindyp89576472012-11-06 17:07:08 -08001553 toAddresses.add(fullSenderAddress);
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001554 } else {
1555 // This happens if the user replies to a message they originally
Mindy Pereira1883b342012-06-20 08:34:56 -07001556 // wrote. In this case, "reply" really means "re-send," so we
1557 // target the original recipients. This works as expected even
1558 // if the user sent the original message to themselves.
mindypfe8557b2012-11-05 12:05:16 -08001559 for (String address : inToAddresses) {
mindyp5ee5d692012-11-19 16:02:16 -08001560 if (!recipientMatchesThisAccount(address)) {
mindypfe8557b2012-11-05 12:05:16 -08001561 toAddresses.add(address);
1562 }
1563 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001564 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001565 }
1566 return toAddresses;
1567 }
1568
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001569 private void addRecipients(String accountAddress, Set<String> recipients, String[] addresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001570 for (String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001571 // Do not add this account, or any of its custom from addresses, to
1572 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001573 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08001574 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001575 recipients.add(email.replace("\"\"", ""));
1576 }
1577 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001578 }
1579
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001580 /**
1581 * A recipient matches this account if it has the same address as the
1582 * currently selected account OR one of the custom from addresses associated
1583 * with the currently selected account.
1584 * @param accountAddress currently selected account
1585 * @param recipientAddress address we are comparing with the currently selected account
1586 * @return
1587 */
mindyp5ee5d692012-11-19 16:02:16 -08001588 protected boolean recipientMatchesThisAccount(String recipientAddress) {
1589 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08001590 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001591 }
1592
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001593 private void setSubject(Message refMessage, int action) {
1594 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001595 String prefix;
1596 String correctedSubject = null;
1597 if (action == ComposeActivity.COMPOSE) {
1598 prefix = "";
1599 } else if (action == ComposeActivity.FORWARD) {
1600 prefix = getString(R.string.forward_subject_label);
1601 } else {
1602 prefix = getString(R.string.reply_subject_label);
1603 }
1604
1605 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07001606 if (!TextUtils.isEmpty(subject)
1607 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001608 correctedSubject = subject;
1609 } else {
1610 correctedSubject = String
1611 .format(getString(R.string.formatted_subject), prefix, subject);
1612 }
1613 mSubject.setText(correctedSubject);
1614 }
1615
Mindy Pereira818143e2012-01-11 13:59:49 -08001616 private void initRecipients() {
1617 setupRecipients(mTo);
1618 setupRecipients(mCc);
1619 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001620 }
1621
Mindy Pereira818143e2012-01-11 13:59:49 -08001622 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001623 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -08001624 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001625 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001626 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001627 int offset = accountName.indexOf("@") + 1;
1628 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001629 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001630 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001631 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001632 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001633 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001634 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001635 }
1636
1637 @Override
1638 public void onClick(View v) {
1639 int id = v.getId();
1640 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001641 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001642 // Verify that cc/ bcc aren't showing.
1643 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001644 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001645 break;
mindypcd0b0b92012-08-23 14:33:17 -07001646 case R.id.add_photo_attachment:
1647 doAttach(MIME_TYPE_PHOTO);
1648 break;
1649 case R.id.add_video_attachment:
1650 doAttach(MIME_TYPE_VIDEO);
1651 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001652 }
1653 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001654
1655 @Override
1656 public boolean onCreateOptionsMenu(Menu menu) {
1657 super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07001658 // Don't render any menu items when there are no accounts.
1659 if (mAccounts == null || mAccounts.length == 0) {
1660 return true;
1661 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001662 MenuInflater inflater = getMenuInflater();
1663 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001664 mSave = menu.findItem(R.id.save);
1665 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001666 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1667 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1668 if (helpItem != null) {
1669 helpItem.setVisible(mAccount != null
1670 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1671 }
1672 if (sendFeedbackItem != null) {
1673 sendFeedbackItem.setVisible(mAccount != null
1674 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1675 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001676 return true;
1677 }
1678
1679 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001680 public boolean onPrepareOptionsMenu(Menu menu) {
1681 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001682 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001683 // Its possible there is a menu item OR a button.
1684 boolean ccFieldVisible = mCc.isShown();
1685 boolean bccFieldVisible = mBcc.isShown();
1686 if (!ccFieldVisible || !bccFieldVisible) {
1687 ccBcc.setVisible(true);
1688 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1689 : R.string.add_bcc_label));
1690 } else {
1691 ccBcc.setVisible(false);
1692 }
1693 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001694 if (mSave != null) {
1695 mSave.setEnabled(shouldSave());
1696 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001697 return true;
1698 }
1699
1700 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001701 public boolean onOptionsItemSelected(MenuItem item) {
1702 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001703 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001704 switch (id) {
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07001705 case R.id.add_photo_attachment:
1706 doAttach(MIME_TYPE_PHOTO);
1707 break;
1708 case R.id.add_video_attachment:
1709 doAttach(MIME_TYPE_VIDEO);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001710 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001711 case R.id.add_cc_bcc:
1712 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001713 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001714 case R.id.save:
Mindy Pereira48e31b02012-05-30 13:12:24 -07001715 doSave(true);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001716 break;
1717 case R.id.send:
1718 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001719 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001720 case R.id.discard:
1721 doDiscard();
1722 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001723 case R.id.settings:
1724 Utils.showSettings(this, mAccount);
1725 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001726 case android.R.id.home:
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001727 onAppUpPressed();
Mindy Pereirafbe40192012-03-20 10:40:45 -07001728 break;
1729 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -07001730 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
Mindy Pereirafbe40192012-03-20 10:40:45 -07001731 break;
1732 case R.id.feedback_menu_item:
Paul Westbrook17beb0b2012-08-20 13:34:37 -07001733 Utils.sendFeedback(this, mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001734 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001735 default:
1736 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001737 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001738 }
1739 return !handled ? super.onOptionsItemSelected(item) : handled;
1740 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001741
Mindy Pereirab199d172012-08-13 11:04:03 -07001742 @Override
1743 public void onBackPressed() {
1744 // If we are showing the wait fragment, just exit.
1745 if (getWaitFragment() != null) {
1746 finish();
1747 } else {
1748 super.onBackPressed();
1749 }
1750 }
1751
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001752 /**
1753 * Carries out the "up" action in the action bar.
1754 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001755 private void onAppUpPressed() {
1756 if (mLaunchedFromEmail) {
1757 // If this was started from Gmail, simply treat app up as the system back button, so
1758 // that the last view is restored.
1759 onBackPressed();
1760 return;
1761 }
1762
1763 // Fire the main activity to ensure it launches the "top" screen of mail.
1764 // Since the main Activity is singleTask, it should revive that task if it was already
1765 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07001766 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001767 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
1768 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
1769 startActivity(mailIntent);
1770 finish();
1771 }
1772
Mindy Pereira33fe9082012-01-09 16:24:30 -08001773 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07001774 sendOrSaveWithSanityChecks(false, true, false, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001775 }
1776
Mindy Pereira48e31b02012-05-30 13:12:24 -07001777 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07001778 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07001779 }
1780
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001781 @VisibleForTesting
1782 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001783 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001784 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1785 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001786 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1787 }
1788
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001789 @VisibleForTesting
1790 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001791 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001792 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001793 public final SendOrSaveCallback mSendOrSaveCallback;
1794 @VisibleForTesting
1795 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08001796 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001797
1798 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08001799 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001800 mContext = context;
1801 mSendOrSaveCallback = callback;
1802 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08001803 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001804 }
1805
1806 @Override
1807 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001808 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001809
Mindy Pereira92551d02012-04-05 11:31:12 -07001810 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001811 Message message = mSendOrSaveCallback.getMessage();
1812 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001813 // If a previous draft has been saved, in an account that is different
1814 // than what the user wants to send from, remove the old draft, and treat this
1815 // as a new message
mindyp44a63392012-11-05 12:05:16 -08001816 if (mExistingDraftAccount != null
1817 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001818 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1819 ContentResolver resolver = mContext.getContentResolver();
1820 ContentValues values = new ContentValues();
1821 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08001822 if (mExistingDraftAccount.account.expungeMessageUri != null) {
1823 new ContentProviderTask.UpdateTask()
1824 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
1825 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001826 } else {
1827 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001828 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001829 // reset messageId to 0, so a new message will be created
1830 messageId = UIProvider.INVALID_MESSAGE_ID;
1831 }
1832 }
1833
1834 final long messageIdToSave = messageId;
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001835 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount, message);
1836
1837 if (!sendOrSaveMessage.mSave) {
1838 UIProvider.incrementRecipientsTimesContacted(mContext,
1839 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
1840 UIProvider.incrementRecipientsTimesContacted(mContext,
1841 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
1842 UIProvider.incrementRecipientsTimesContacted(mContext,
1843 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
1844 }
1845 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1846 }
1847
1848 /**
1849 * Send or Save a message.
1850 */
1851 private void sendOrSaveMessage(long messageIdToSave, SendOrSaveMessage sendOrSaveMessage,
1852 ReplyFromAccount selectedAccount, Message message) {
1853 final ContentResolver resolver = mContext.getContentResolver();
1854 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
1855
1856 final String accountMethod = sendOrSaveMessage.mSave ?
1857 UIProvider.AccountCallMethods.SAVE_MESSAGE :
1858 UIProvider.AccountCallMethods.SEND_MESSAGE;
1859
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001860 try {
1861 if (updateExistingMessage) {
1862 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001863
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001864 final Bundle result = callAccountSendSaveMethod(resolver,
1865 selectedAccount.account, accountMethod, sendOrSaveMessage);
1866 if (result == null) {
1867 // TODO(pwestbro): Once Email supports the call api, remove this block
1868 // If null was returned, then the provider didn't handle the call method
1869 final Uri updateUri = Uri.parse(sendOrSaveMessage.mSave ?
1870 message.saveUri : message.sendUri);
1871 resolver.update(updateUri, sendOrSaveMessage.mValues, null, null);
1872 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001873 } else {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001874 final Uri messageUri;
1875 final Bundle result = callAccountSendSaveMethod(resolver,
1876 selectedAccount.account, accountMethod, sendOrSaveMessage);
1877 if (result != null) {
1878 // If a non-null value was returned, then the provider handled the call
1879 // method
1880 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
1881 } else {
1882 // TODO(pwestbro): Once Email supports the call api, remove this block
1883 messageUri = resolver.insert(
1884 sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
1885 : selectedAccount.account.sendMessageUri,
1886 sendOrSaveMessage.mValues);
1887 }
1888 if (sendOrSaveMessage.mSave && messageUri != null) {
1889 final Cursor messageCursor = resolver.query(messageUri,
1890 UIProvider.MESSAGE_PROJECTION, null, null, null);
1891 if (messageCursor != null) {
1892 try {
1893 if (messageCursor.moveToFirst()) {
1894 // Broadcast notification that a new message has
1895 // been allocated
1896 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1897 new Message(messageCursor));
1898 }
1899 } finally {
1900 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07001901 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001902 }
1903 }
1904 }
1905 } finally {
1906 // Close any opened file descriptors
1907 closeOpenedAttachmentFds(sendOrSaveMessage);
1908 }
1909 }
1910
1911 private void closeOpenedAttachmentFds(SendOrSaveMessage sendOrSaveMessage) {
1912 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
1913 if (openedFds != null) {
1914 final Set<String> keys = openedFds.keySet();
1915 for (String key : keys) {
1916 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
1917 if (fd != null) {
1918 try {
1919 fd.close();
1920 } catch (IOException e) {
1921 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07001922 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001923 }
1924 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001925 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001926 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001927
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001928 /**
1929 * Use the {@link ContentResolver#call()} method to send or save the message.
1930 *
1931 * If this was successful, this method will return an non-null Bundle instance
1932 */
1933 private Bundle callAccountSendSaveMethod(ContentResolver resolver, Account account,
1934 String method, SendOrSaveMessage sendOrSaveMessage) {
1935 // Copy all of the values from the content values to the bundle
1936 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
1937 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
1938
1939 for (Entry<String, Object> entry : valueSet) {
1940 final Object entryValue = entry.getValue();
1941 final String key = entry.getKey();
1942 if (entryValue instanceof String) {
1943 methodExtras.putString(key, (String)entryValue);
1944 } else if (entryValue instanceof Boolean) {
1945 methodExtras.putBoolean(key, (Boolean)entryValue);
1946 } else if (entryValue instanceof Integer) {
1947 methodExtras.putInt(key, (Integer)entryValue);
1948 } else if (entryValue instanceof Long) {
1949 methodExtras.putLong(key, (Long)entryValue);
1950 } else {
1951 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
1952 entryValue.getClass().getName());
1953 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001954 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001955
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001956 // If the SendOrSaveMessage has some opened fds, add them to the bundle
1957 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
1958 if (fdMap != null) {
1959 methodExtras.putParcelable(
1960 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
1961 }
1962
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001963 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001964 }
1965 }
1966
1967 // Array of the outstanding send or save tasks. Access is synchronized
1968 // with the object itself
1969 /* package for testing */
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001970 @VisibleForTesting
1971 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001972 // FIXME: this variable is never read. related to sRequestMessageIdMap.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001973 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001974 private String mSignature;
Mindy Pereirab199d172012-08-13 11:04:03 -07001975 private Account[] mAccounts;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001976
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001977 @VisibleForTesting
1978 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07001979 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001980 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001981 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001982 @VisibleForTesting
1983 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001984 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001985 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001986
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001987 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
1988 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001989 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001990 mValues = values;
1991 mRefMessageId = refMessageId;
1992 mSave = save;
1993 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07001994
1995 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001996 }
1997
1998 int requestId() {
1999 return mRequestId;
2000 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002001
2002 Bundle attachmentFds() {
2003 return mAttachmentFds;
2004 }
2005
2006 /**
2007 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2008 * called before the ComposeActivity finishes.
2009 * Note: The caller is responsible for closing these file descriptors.
2010 */
2011 private Bundle initializeAttachmentFds(Context context, List<Attachment> attachments) {
2012 if (attachments == null || attachments.size() == 0) {
2013 return null;
2014 }
2015
2016 final Bundle result = new Bundle(attachments.size());
2017 final ContentResolver resolver = context.getContentResolver();
2018
2019 for (Attachment attachment : attachments) {
2020 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2021 continue;
2022 }
2023
2024 ParcelFileDescriptor fileDescriptor;
2025 try {
2026 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2027 } catch (FileNotFoundException e) {
2028 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2029 fileDescriptor = null;
2030 }
2031
2032 if (fileDescriptor != null) {
2033 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2034 }
2035 }
2036
2037 return result;
2038 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002039 }
2040
2041 /**
2042 * Get the to recipients.
2043 */
2044 public String[] getToAddresses() {
2045 return getAddressesFromList(mTo);
2046 }
2047
2048 /**
2049 * Get the cc recipients.
2050 */
2051 public String[] getCcAddresses() {
2052 return getAddressesFromList(mCc);
2053 }
2054
2055 /**
2056 * Get the bcc recipients.
2057 */
2058 public String[] getBccAddresses() {
2059 return getAddressesFromList(mBcc);
2060 }
2061
2062 public String[] getAddressesFromList(RecipientEditTextView list) {
2063 if (list == null) {
2064 return new String[0];
2065 }
2066 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2067 int count = tokens.length;
2068 String[] result = new String[count];
2069 for (int i = 0; i < count; i++) {
2070 result[i] = tokens[i].toString();
2071 }
2072 return result;
2073 }
2074
2075 /**
2076 * Check for invalid email addresses.
2077 * @param to String array of email addresses to check.
2078 * @param wrongEmailsOut Emails addresses that were invalid.
2079 */
2080 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002081 if (mValidator == null) {
2082 return;
2083 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002084 for (String email : to) {
2085 if (!mValidator.isValid(email)) {
2086 wrongEmailsOut.add(email);
2087 }
2088 }
2089 }
2090
2091 /**
2092 * Show an error because the user has entered an invalid recipient.
2093 * @param message
2094 */
2095 public void showRecipientErrorDialog(String message) {
2096 // Only 1 invalid recipients error dialog should be allowed up at a
2097 // time.
2098 if (mRecipientErrorDialog != null) {
2099 mRecipientErrorDialog.dismiss();
2100 }
2101 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
2102 R.string.recipient_error_dialog_title)
2103 .setIconAttribute(android.R.attr.alertDialogIcon)
Mindy Pereira82cc5662012-01-09 17:29:30 -08002104 .setPositiveButton(
2105 R.string.ok, new Dialog.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07002106 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002107 public void onClick(DialogInterface dialog, int which) {
2108 // after the user dismisses the recipient error
2109 // dialog we want to make sure to refocus the
2110 // recipient to field so they can fix the issue
2111 // easily
2112 if (mTo != null) {
2113 mTo.requestFocus();
2114 }
2115 mRecipientErrorDialog = null;
2116 }
2117 }).show();
2118 }
2119
2120 /**
2121 * Update the state of the UI based on whether or not the current draft
2122 * needs to be saved and the message is not empty.
2123 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002124 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002125 if (mSave != null) {
2126 mSave.setEnabled((shouldSave() && !isBlank()));
2127 }
2128 }
2129
2130 /**
2131 * Returns true if we need to save the current draft.
2132 */
2133 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002134 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002135 // The message should only be saved if:
2136 // It hasn't been sent AND
2137 // Some text has been added to the message OR
2138 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002139 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002140 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002141 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002142 }
2143 }
2144
2145 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002146 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002147 * @return boolean
2148 */
2149 public boolean isBlank() {
2150 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002151 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2152 mBodyView.getText().toString()) == 0)
2153 && mTo.length() == 0
2154 && mCc.length() == 0 && mBcc.length() == 0
2155 && mAttachmentsView.getAttachments().size() == 0;
2156 }
2157
2158 @VisibleForTesting
2159 protected int getSignatureStartPosition(String signature, String bodyText) {
2160 int startPos = -1;
2161
2162 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2163 return startPos;
2164 }
2165
2166 int bodyLength = bodyText.length();
2167 int signatureLength = signature.length();
2168 String printableVersion = convertToPrintableSignature(signature);
2169 int printableLength = printableVersion.length();
2170
2171 if (bodyLength >= printableLength
2172 && bodyText.substring(bodyLength - printableLength)
2173 .equals(printableVersion)) {
2174 startPos = bodyLength - printableLength;
2175 } else if (bodyLength >= signatureLength
2176 && bodyText.substring(bodyLength - signatureLength)
2177 .equals(signature)) {
2178 startPos = bodyLength - signatureLength;
2179 }
2180 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002181 }
2182
2183 /**
2184 * Allows any changes made by the user to be ignored. Called when the user
2185 * decides to discard a draft.
2186 */
2187 private void discardChanges() {
2188 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002189 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002190 mReplyFromChanged = false;
2191 }
2192
2193 /**
Mindy Pereira181df782012-03-01 13:32:44 -08002194 * @param body
2195 * @param save
2196 * @param showToast
2197 * @return Whether the send or save succeeded.
2198 */
2199 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002200 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002201 if (mAccounts == null || mAccount == null) {
2202 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002203 if (autoSend) {
2204 finish();
2205 }
Mark Wei009b3712012-10-18 18:07:50 -07002206 return false;
2207 }
2208
Mindy Pereira181df782012-03-01 13:32:44 -08002209 String[] to, cc, bcc;
2210 Editable body = mBodyView.getEditableText();
Mindy Pereira181df782012-03-01 13:32:44 -08002211 if (orientationChanged) {
2212 to = cc = bcc = new String[0];
2213 } else {
2214 to = getToAddresses();
2215 cc = getCcAddresses();
2216 bcc = getBccAddresses();
2217 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002218
Mindy Pereira181df782012-03-01 13:32:44 -08002219 // Don't let the user send to nobody (but it's okay to save a message
2220 // with no recipients)
2221 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2222 showRecipientErrorDialog(getString(R.string.recipient_needed));
2223 return false;
2224 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002225
Mindy Pereira181df782012-03-01 13:32:44 -08002226 List<String> wrongEmails = new ArrayList<String>();
2227 if (!save) {
2228 checkInvalidEmails(to, wrongEmails);
2229 checkInvalidEmails(cc, wrongEmails);
2230 checkInvalidEmails(bcc, wrongEmails);
2231 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002232
Mindy Pereira181df782012-03-01 13:32:44 -08002233 // Don't let the user send an email with invalid recipients
2234 if (wrongEmails.size() > 0) {
2235 String errorText = String.format(getString(R.string.invalid_recipient),
2236 wrongEmails.get(0));
2237 showRecipientErrorDialog(errorText);
2238 return false;
2239 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002240
Mindy Pereira181df782012-03-01 13:32:44 -08002241 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07002242 @Override
Mindy Pereira181df782012-03-01 13:32:44 -08002243 public void onClick(DialogInterface dialog, int which) {
2244 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
2245 }
2246 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002247
Mindy Pereira181df782012-03-01 13:32:44 -08002248 // Show a warning before sending only if there are no attachments.
2249 if (!save) {
2250 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2251 boolean warnAboutEmptySubject = isSubjectEmpty();
2252 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002253
Mindy Pereira181df782012-03-01 13:32:44 -08002254 // A warning about an empty body may not be warranted when
2255 // forwarding mails, since a common use case is to forward
2256 // quoted text and not append any more text.
2257 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002258
Mindy Pereira181df782012-03-01 13:32:44 -08002259 // When we bring up a dialog warning the user about a send,
2260 // assume that they accept sending the message. If they do not,
2261 // the dialog listener is required to enable sending again.
2262 if (warnAboutEmptySubject) {
2263 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
2264 return true;
2265 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002266
Mindy Pereira181df782012-03-01 13:32:44 -08002267 if (warnAboutEmptyBody) {
2268 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
2269 return true;
2270 }
2271 }
2272 // Ask for confirmation to send (if always required)
2273 if (showSendConfirmation()) {
2274 showSendConfirmDialog(R.string.confirm_send_message, listener);
2275 return true;
2276 }
2277 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002278
Mindy Pereira181df782012-03-01 13:32:44 -08002279 sendOrSave(body, save, showToast, false);
2280 return true;
2281 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002282
Mindy Pereira181df782012-03-01 13:32:44 -08002283 /**
2284 * Returns a boolean indicating whether warnings should be shown for empty
2285 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002286 *
Mindy Pereira181df782012-03-01 13:32:44 -08002287 * @return True if a warning should be shown for empty text fields
2288 */
2289 protected boolean showEmptyTextWarnings() {
2290 return mAttachmentsView.getAttachments().size() == 0;
2291 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002292
Mindy Pereira181df782012-03-01 13:32:44 -08002293 /**
2294 * Returns a boolean indicating whether the user should confirm each send
2295 *
2296 * @return True if a warning should be on each send
2297 */
2298 protected boolean showSendConfirmation() {
2299 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2300 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002301
Mindy Pereira181df782012-03-01 13:32:44 -08002302 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
2303 if (mSendConfirmDialog != null) {
2304 mSendConfirmDialog.dismiss();
2305 mSendConfirmDialog = null;
2306 }
2307 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
2308 .setTitle(R.string.confirm_send_title)
2309 .setIconAttribute(android.R.attr.alertDialogIcon)
2310 .setPositiveButton(R.string.send, listener)
Mindy Pereira6edd5972012-06-19 10:22:36 -07002311 .setNegativeButton(R.string.cancel, this)
2312 .show();
Mindy Pereira181df782012-03-01 13:32:44 -08002313 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002314
Mindy Pereira181df782012-03-01 13:32:44 -08002315 /**
2316 * Returns whether the ComposeArea believes there is any text in the body of
2317 * the composition. TODO: When ComposeArea controls the Body as well, add
2318 * that here.
2319 */
2320 public boolean isBodyEmpty() {
2321 return !mQuotedTextView.isTextIncluded();
2322 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002323
Mindy Pereira181df782012-03-01 13:32:44 -08002324 /**
2325 * Test to see if the subject is empty.
2326 *
2327 * @return boolean.
2328 */
2329 // TODO: this will likely go away when composeArea.focus() is implemented
2330 // after all the widget control is moved over.
2331 public boolean isSubjectEmpty() {
2332 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2333 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002334
Mindy Pereira181df782012-03-01 13:32:44 -08002335 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002336 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07002337 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08002338 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
2339 ReplyFromAccount draftAccount) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002340 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002341
Mindy Pereirac2031972012-04-03 09:38:35 -07002342 String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
2343
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002344 MessageModification.putToAddresses(values, message.getToAddresses());
2345 MessageModification.putCcAddresses(values, message.getCcAddresses());
2346 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002347
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002348 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07002349
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002350 MessageModification.putSubject(values, message.subject);
mindyped9c2f02012-10-12 10:02:08 -07002351 String htmlBody = Html.toHtml(new SpannableString(body.toString()));
Paul Westbrook05b92b82012-04-20 13:29:37 -07002352
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002353 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2354 StringBuilder fullBody = new StringBuilder(htmlBody);
2355 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002356 // HTML gets converted to text for now
2357 final String text = quotedText.toString();
2358 if (QuotedTextView.containsQuotedText(text)) {
2359 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07002360 final int quoteStartPos = fullBody.length() + pos;
2361 fullBody.append(text);
2362 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07002363 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07002364 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002365 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002366 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2367 // This shouldn't happen, but just use what we have,
2368 // and don't do server-side expansion
2369 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002370 }
2371 }
Mindy Pereira002ff522012-05-30 10:31:26 -07002372 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07002373 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002374 if (refMessage != null) {
2375 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2376 MessageModification.putBodyHtml(values, fullBody.toString());
2377 }
2378 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08002379 MessageModification.putBody(values,
2380 Utils.convertHtmlToPlainText(fullBody.toString()).toString());
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002381 }
2382 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002383 MessageModification.putBodyHtml(values, fullBody.toString());
mindypc59dd822012-11-13 10:56:21 -08002384 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2385 .toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002386 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002387 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002388 if (!TextUtils.isEmpty(refMessageId)) {
2389 MessageModification.putRefMessageId(values, refMessageId);
2390 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002391 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2392 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08002393 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2394 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002395
Mindy Pereira181df782012-03-01 13:32:44 -08002396 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08002397 // Do the send/save action on the specified handler to avoid possible
2398 // ANRs
2399 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002400
Mindy Pereira181df782012-03-01 13:32:44 -08002401 return sendOrSaveMessage.requestId();
2402 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002403
Mindy Pereira002ff522012-05-30 10:31:26 -07002404 private static int getDraftType(int mode) {
2405 int draftType = -1;
2406 switch (mode) {
2407 case ComposeActivity.COMPOSE:
2408 draftType = DraftType.COMPOSE;
2409 break;
2410 case ComposeActivity.REPLY:
2411 draftType = DraftType.REPLY;
2412 break;
2413 case ComposeActivity.REPLY_ALL:
2414 draftType = DraftType.REPLY_ALL;
2415 break;
2416 case ComposeActivity.FORWARD:
2417 draftType = DraftType.FORWARD;
2418 break;
2419 }
2420 return draftType;
2421 }
2422
Mindy Pereira181df782012-03-01 13:32:44 -08002423 private void sendOrSave(Spanned body, boolean save, boolean showToast,
2424 boolean orientationChanged) {
2425 // Check if user is a monkey. Monkeys can compose and hit send
2426 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002427 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002428 return;
2429 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002430
Mindy Pereira181df782012-03-01 13:32:44 -08002431 String[] to, cc, bcc;
2432 if (orientationChanged) {
2433 to = cc = bcc = new String[0];
2434 } else {
2435 to = getToAddresses();
2436 cc = getCcAddresses();
2437 bcc = getBccAddresses();
2438 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002439
Mindy Pereira181df782012-03-01 13:32:44 -08002440 SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07002441 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08002442 private int mRestoredRequestId;
2443
Marc Blank0bbc8582012-04-23 15:07:57 -07002444 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002445 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002446 synchronized (mActiveTasks) {
2447 int numTasks = mActiveTasks.size();
2448 if (numTasks == 0) {
2449 // Start service so we won't be killed if this app is
2450 // put in the background.
2451 startService(new Intent(ComposeActivity.this, EmptyService.class));
2452 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002453
Mindy Pereira181df782012-03-01 13:32:44 -08002454 mActiveTasks.add(sendOrSaveTask);
2455 }
2456 if (sTestSendOrSaveCallback != null) {
2457 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2458 }
2459 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002460
Marc Blank0bbc8582012-04-23 15:07:57 -07002461 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002462 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2463 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002464 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08002465 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002466 mDraftId = message.id;
2467 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002468 if (sRequestMessageIdMap != null) {
2469 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2470 }
Mindy Pereira181df782012-03-01 13:32:44 -08002471 // Cache request message map, in case the process is killed
2472 saveRequestMap();
2473 }
2474 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002475 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002476 }
2477 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002478
Marc Blank0bbc8582012-04-23 15:07:57 -07002479 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002480 public Message getMessage() {
2481 synchronized (mDraftLock) {
2482 return mDraft;
2483 }
2484 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002485
Marc Blank0bbc8582012-04-23 15:07:57 -07002486 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002487 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07002488 // Update the last sent from account.
2489 if (mAccount != null) {
2490 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2491 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002492 if (success) {
2493 // Successfully sent or saved so reset change markers
2494 discardChanges();
2495 } else {
2496 // A failure happened with saving/sending the draft
2497 // TODO(pwestbro): add a better string that should be used
2498 // when failing to send or save
2499 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2500 .show();
2501 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002502
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002503 int numTasks;
2504 synchronized (mActiveTasks) {
2505 // Remove the task from the list of active tasks
2506 mActiveTasks.remove(task);
2507 numTasks = mActiveTasks.size();
2508 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002509
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002510 if (numTasks == 0) {
2511 // Stop service so we can be killed.
2512 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2513 }
2514 if (sTestSendOrSaveCallback != null) {
2515 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2516 }
2517 }
Mindy Pereira181df782012-03-01 13:32:44 -08002518 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002519
Mindy Pereira181df782012-03-01 13:32:44 -08002520 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07002521 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002522 String fromAddress = selectedAccount.name;
2523 if (selectedAccount == null || fromAddress == null) {
2524 // We don't have either the selected account or from address,
2525 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07002526 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002527 fromAddress = mAccount.name;
2528 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002529
Mindy Pereira181df782012-03-01 13:32:44 -08002530 if (mSendSaveTaskHandler == null) {
2531 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2532 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002533
Mindy Pereira181df782012-03-01 13:32:44 -08002534 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2535 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002536
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002537 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002538 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2539 mQuotedTextView.getQuotedTextIfIncluded(), callback,
mindyp44a63392012-11-05 12:05:16 -08002540 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002541
Mindy Pereira181df782012-03-01 13:32:44 -08002542 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
2543 mRecipient = selectedAccount.name;
2544 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002545 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002546
Mindy Pereira181df782012-03-01 13:32:44 -08002547 // Don't display the toast if the user is just changing the orientation,
2548 // but we still need to save the draft to the cursor because this is how we restore
2549 // the attachments when the configuration change completes.
2550 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2551 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2552 Toast.LENGTH_LONG).show();
2553 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002554
Mindy Pereira181df782012-03-01 13:32:44 -08002555 // Need to update variables here because the send or save completes
2556 // asynchronously even though the toast shows right away.
2557 discardChanges();
2558 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002559
Mindy Pereira181df782012-03-01 13:32:44 -08002560 // If we are sending, finish the activity
2561 if (!save) {
2562 finish();
2563 }
2564 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002565
Mindy Pereira181df782012-03-01 13:32:44 -08002566 /**
2567 * Save the state of the request messageid map. This allows for the Gmail
2568 * process to be killed, but and still allow for ComposeActivity instances
2569 * to be recreated correctly.
2570 */
2571 private void saveRequestMap() {
2572 // TODO: store the request map in user preferences.
2573 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002574
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002575 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002576 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2577 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002578 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002579 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08002580 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002581 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2582 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002583 }
2584
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002585 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002586 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002587 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07002588 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002589 }
2590 }
2591
Mindy Pereira326c6602012-01-04 15:32:42 -08002592 @Override
2593 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002594 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002595 if (position == ComposeActivity.REPLY) {
2596 mComposeMode = ComposeActivity.REPLY;
2597 } else if (position == ComposeActivity.REPLY_ALL) {
2598 mComposeMode = ComposeActivity.REPLY_ALL;
2599 } else if (position == ComposeActivity.FORWARD) {
2600 mComposeMode = ComposeActivity.FORWARD;
2601 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002602 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002603 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002604 resetMessageForModeChange();
Mindy Pereiraef388302012-06-18 19:07:44 -07002605 if (mDraft == null && mRefMessage != null) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07002606 setFieldsFromRefMessage(mComposeMode, mAccount.name);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002607 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002608 boolean showCc = false;
2609 boolean showBcc = false;
2610 if (mDraft != null) {
2611 // Following desktop behavior, if the user has added a BCC
2612 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002613 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07002614 // Use the draft to determine what to populate.
2615 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002616 showCc = showBcc
2617 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
Mindy Pereiraef388302012-06-18 19:07:44 -07002618 } else if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07002619 showCc = !TextUtils.isEmpty(mCc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07002620 }
2621 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002622 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002623 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002624 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08002625 return true;
2626 }
2627
Mindy Pereirab3112a22012-06-20 12:10:03 -07002628 @VisibleForTesting
2629 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08002630 // When switching between reply, reply all, forward,
2631 // follow the behavior of webview.
2632 // The contents of the following fields are cleared
2633 // so that they can be populated directly from the
2634 // ref message:
2635 // 1) Any recipient fields
2636 // 2) The subject
2637 mTo.setText("");
2638 mCc.setText("");
2639 mBcc.setText("");
2640 // Any edits to the subject are replaced with the original subject.
2641 mSubject.setText("");
2642
2643 // Any changes to the contents of the following fields are kept:
2644 // 1) Body
2645 // 2) Attachments
2646 // If the user made changes to attachments, keep their changes.
2647 if (!mAttachmentsChanged) {
2648 mAttachmentsView.deleteAllAttachments();
2649 }
2650 }
2651
Mindy Pereira326c6602012-01-04 15:32:42 -08002652 private class ComposeModeAdapter extends ArrayAdapter<String> {
2653
2654 private LayoutInflater mInflater;
2655
2656 public ComposeModeAdapter(Context context) {
2657 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2658 .getStringArray(R.array.compose_modes));
2659 }
2660
2661 private LayoutInflater getInflater() {
2662 if (mInflater == null) {
2663 mInflater = LayoutInflater.from(getContext());
2664 }
2665 return mInflater;
2666 }
2667
2668 @Override
2669 public View getView(int position, View convertView, ViewGroup parent) {
2670 if (convertView == null) {
2671 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2672 }
2673 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2674 return super.getView(position, convertView, parent);
2675 }
2676 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002677
2678 @Override
2679 public void onRespondInline(String text) {
2680 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07002681 mQuotedTextView.setUpperDividerVisible(false);
2682 mTo.requestFocus();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002683 }
2684
2685 /**
2686 * Append text to the body of the message. If there is no existing body
2687 * text, just sets the body to text.
2688 *
2689 * @param text
2690 * @param withSignature True to append a signature.
2691 */
2692 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002693 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002694 if (bodyText != null && bodyText.length() > 0) {
2695 bodyText.append(text);
2696 } else {
2697 setBody(text, withSignature);
2698 }
2699 }
2700
2701 /**
2702 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002703 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002704 * @param text
2705 * @param withSignature True to append a signature.
2706 */
2707 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002708 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002709 if (withSignature) {
2710 appendSignature();
2711 }
2712 }
2713
2714 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002715 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002716 boolean hasFocus = mBodyView.hasFocus();
mindyp27083062012-11-15 09:02:01 -08002717 int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
2718 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002719 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08002720 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002721 // Appending a signature does not count as changing text.
2722 mBodyView.removeTextChangedListener(this);
2723 mBodyView.append(convertToPrintableSignature(mSignature));
2724 mBodyView.addTextChangedListener(this);
2725 }
Mindy Pereira433b1982012-04-03 11:53:07 -07002726 if (hasFocus) {
2727 focusBody();
2728 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002729 }
2730 }
2731
2732 private String convertToPrintableSignature(String signature) {
2733 String signatureResource = getResources().getString(R.string.signature);
2734 if (signature == null) {
2735 signature = "";
2736 }
2737 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002738 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002739
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08002740 @Override
2741 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07002742 mReplyFromAccount = mFromSpinner.getCurrentAccount();
2743 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08002744 // Clear a signature, if there was one.
2745 mBodyView.removeTextChangedListener(this);
2746 String oldSignature = mSignature;
2747 String bodyText = getBody().getText().toString();
2748 if (!TextUtils.isEmpty(oldSignature)) {
2749 int pos = getSignatureStartPosition(oldSignature, bodyText);
2750 if (pos > -1) {
2751 mBodyView.setText(bodyText.substring(0, pos));
2752 }
2753 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002754 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08002755 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08002756 // TODO: handle discarding attachments when switching accounts.
2757 // Only enable save for this draft if there is any other content
2758 // in the message.
2759 if (!isBlank()) {
2760 enableSave(true);
2761 }
2762 mReplyFromChanged = true;
2763 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002764 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002765 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002766
2767 public void enableSave(boolean enabled) {
2768 if (mSave != null) {
2769 mSave.setEnabled(enabled);
2770 }
2771 }
2772
2773 public void enableSend(boolean enabled) {
2774 if (mSend != null) {
2775 mSend.setEnabled(enabled);
2776 }
2777 }
2778
2779 /**
2780 * Handles button clicks from any error dialogs dealing with sending
2781 * a message.
2782 */
2783 @Override
2784 public void onClick(DialogInterface dialog, int which) {
2785 switch (which) {
2786 case DialogInterface.BUTTON_POSITIVE: {
2787 doDiscardWithoutConfirmation(true /* show toast */ );
2788 break;
2789 }
2790 case DialogInterface.BUTTON_NEGATIVE: {
2791 // If the user cancels the send, re-enable the send button.
2792 enableSend(true);
2793 break;
2794 }
2795 }
2796
2797 }
2798
Mindy Pereiraefe3d252012-03-01 14:20:44 -08002799 private void doDiscard() {
2800 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2801 .setPositiveButton(R.string.ok, this)
2802 .setNegativeButton(R.string.cancel, null)
2803 .create().show();
2804 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002805 /**
2806 * Effectively discard the current message.
2807 *
2808 * This method is either invoked from the menu or from the dialog
2809 * once the user has confirmed that they want to discard the message.
2810 * @param showToast show "Message discarded" toast if true
2811 */
2812 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002813 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002814 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2815 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07002816 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07002817 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002818 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2819 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07002820 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002821 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002822 // This is not strictly necessary (since we should not try to
2823 // save the draft after calling this) but it ensures that if we
2824 // do save again for some reason we make a new draft rather than
2825 // trying to resave an expunged draft.
2826 mDraftId = UIProvider.INVALID_MESSAGE_ID;
2827 }
2828 }
2829
2830 if (showToast) {
2831 // Display a toast to let the user know
2832 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2833 }
2834
2835 // This prevents the draft from being saved in onPause().
2836 discardChanges();
2837 finish();
2838 }
2839
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002840 private void saveIfNeeded() {
2841 if (mAccount == null) {
2842 // We have not chosen an account yet so there's no way that we can save. This is ok,
2843 // though, since we are saving our state before AccountsActivity is activated. Thus, the
2844 // user has not interacted with us yet and there is no real state to save.
2845 return;
2846 }
2847
2848 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07002849 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002850 }
2851 }
2852
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002853 @Override
2854 public void onAttachmentDeleted() {
2855 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07002856 // If we are showing any attachments, make sure we have an upper
2857 // divider.
2858 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002859 updateSaveUi();
2860 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002861
mindyp40882432012-09-06 11:07:40 -07002862 @Override
2863 public void onAttachmentAdded() {
2864 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
2865 mAttachmentsView.focusLastAttachment();
2866 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002867
2868 /**
2869 * This is called any time one of our text fields changes.
2870 */
Marc Blank0bbc8582012-04-23 15:07:57 -07002871 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002872 public void afterTextChanged(Editable s) {
2873 mTextChanged = true;
2874 updateSaveUi();
2875 }
2876
2877 @Override
2878 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2879 // Do nothing.
2880 }
2881
Marc Blank0bbc8582012-04-23 15:07:57 -07002882 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002883 public void onTextChanged(CharSequence s, int start, int before, int count) {
2884 // Do nothing.
2885 }
2886
2887
2888 // There is a big difference between the text associated with an address changing
2889 // to add the display name or to format properly and a recipient being added or deleted.
2890 // Make sure we only notify of changes when a recipient has been added or deleted.
2891 private class RecipientTextWatcher implements TextWatcher {
2892 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2893
2894 private RecipientEditTextView mView;
2895
2896 private TextWatcher mListener;
2897
2898 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2899 mView = view;
2900 mListener = listener;
2901 }
2902
2903 @Override
2904 public void afterTextChanged(Editable s) {
2905 if (hasChanged()) {
2906 mListener.afterTextChanged(s);
2907 }
2908 }
2909
2910 private boolean hasChanged() {
2911 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2912 int totalCount = currRecips.length;
2913 int totalPrevCount = 0;
2914 for (Entry<String, Integer> entry : mContent.entrySet()) {
2915 totalPrevCount += entry.getValue();
2916 }
2917 if (totalCount != totalPrevCount) {
2918 return true;
2919 }
2920
2921 for (String recip : currRecips) {
2922 if (!mContent.containsKey(recip)) {
2923 return true;
2924 } else {
2925 int count = mContent.get(recip) - 1;
2926 if (count < 0) {
2927 return true;
2928 } else {
2929 mContent.put(recip, count);
2930 }
2931 }
2932 }
2933 return false;
2934 }
2935
2936 private String[] tokenizeRecips(String[] recips) {
2937 // Tokenize them all and put them in the list.
2938 String[] recipAddresses = new String[recips.length];
2939 for (int i = 0; i < recips.length; i++) {
2940 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2941 }
2942 return recipAddresses;
2943 }
2944
2945 @Override
2946 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2947 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2948 for (String recip : recips) {
2949 if (!mContent.containsKey(recip)) {
2950 mContent.put(recip, 1);
2951 } else {
2952 mContent.put(recip, (mContent.get(recip)) + 1);
2953 }
2954 }
2955 }
2956
2957 @Override
2958 public void onTextChanged(CharSequence s, int start, int before, int count) {
2959 // Do nothing.
2960 }
2961 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002962
2963 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
2964 if (sTestSendOrSaveCallback != null && testCallback != null) {
2965 throw new IllegalStateException("Attempting to register more than one test callback");
2966 }
2967 sTestSendOrSaveCallback = testCallback;
2968 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07002969
2970 @VisibleForTesting
2971 protected ArrayList<Attachment> getAttachments() {
2972 return mAttachmentsView.getAttachments();
2973 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07002974
2975 @Override
2976 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
2977 switch (id) {
2978 case REFERENCE_MESSAGE_LOADER:
2979 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
2980 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07002981 case LOADER_ACCOUNT_CURSOR:
2982 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
2983 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07002984 }
2985 return null;
2986 }
2987
2988 @Override
2989 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07002990 int id = loader.getId();
2991 switch (id) {
2992 case REFERENCE_MESSAGE_LOADER:
2993 if (data != null && data.moveToFirst()) {
2994 mRefMessage = new Message(data);
2995 // We set these based on EXTRA_TO.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002996 mRefMessage.setTo(null);
2997 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07002998 Intent intent = getIntent();
2999 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
3000 initFromRefMessage(action, mAccount.name);
3001 finishSetup(action, intent, null, true);
3002 if (action != FORWARD) {
3003 String to = intent.getStringExtra(EXTRA_TO);
3004 if (!TextUtils.isEmpty(to)) {
3005 clearChangeListeners();
3006 mTo.append(to);
3007 initChangeListeners();
3008 }
3009 }
3010 } else {
3011 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003012 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003013 break;
3014 case LOADER_ACCOUNT_CURSOR:
3015 if (data != null && data.moveToFirst()) {
3016 // there are accounts now!
3017 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003018 final ArrayList<Account> accounts = new ArrayList<Account>();
3019 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003020 do {
3021 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003022 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003023 initializedAccounts.add(account);
3024 }
3025 accounts.add(account);
3026 } while (data.moveToNext());
3027 if (initializedAccounts.size() > 0) {
3028 findViewById(R.id.wait).setVisibility(View.GONE);
3029 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3030 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003031 mAccounts = initializedAccounts.toArray(
3032 new Account[initializedAccounts.size()]);
3033
Mindy Pereirab199d172012-08-13 11:04:03 -07003034 finishCreate();
3035 invalidateOptionsMenu();
3036 } else {
3037 // Show "waiting"
3038 account = accounts.size() > 0 ? accounts.get(0) : null;
3039 showWaitFragment(account);
3040 }
3041 }
3042 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003043 }
3044 }
3045
Mindy Pereirab199d172012-08-13 11:04:03 -07003046 private void showWaitFragment(Account account) {
3047 WaitFragment fragment = getWaitFragment();
3048 if (fragment != null) {
3049 fragment.updateAccount(account);
3050 } else {
3051 findViewById(R.id.wait).setVisibility(View.VISIBLE);
3052 replaceFragment(WaitFragment.newInstance(account, true),
3053 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3054 }
3055 }
3056
3057 private WaitFragment getWaitFragment() {
3058 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3059 }
3060
3061 private int replaceFragment(Fragment fragment, int transition, String tag) {
3062 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
3063 fragmentTransaction.addToBackStack(null);
3064 fragmentTransaction.setTransition(transition);
3065 fragmentTransaction.replace(R.id.wait, fragment, tag);
3066 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3067 return transactionId;
3068 }
3069
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003070 @Override
3071 public void onLoaderReset(Loader<Cursor> arg0) {
3072 // Do nothing.
3073 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003074}