blob: d30a8ec0055251ebad57f1bc4b74a7f9b37c4e6a [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 Pereira6349a042012-01-04 11:25:01 -080025import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080026import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080027import android.content.Context;
Mindy Pereira82cc5662012-01-09 17:29:30 -080028import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080029import android.content.Intent;
Mindy Pereira82cc5662012-01-09 17:29:30 -080030import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080031import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080033import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080034import android.os.Handler;
35import android.os.HandlerThread;
Paul Westbrookf97588b2012-03-20 11:11:37 -070036import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080037import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080038import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.text.Html;
40import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080041import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080042import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080043import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080044import android.text.util.Rfc822Tokenizer;
Mindy Pereira326c6602012-01-04 15:32:42 -080045import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080046import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080049import android.view.View;
50import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070051import android.view.ViewGroup;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080052import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080053import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080054import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070055import android.widget.EditText;
Mindy Pereira1f936682012-03-02 11:30:33 -080056import android.widget.ImageView;
Mindy Pereira6349a042012-01-04 11:25:01 -080057import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080058import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080059
Mindy Pereirac17d0732011-12-29 10:46:19 -080060import com.android.common.Rfc822Validator;
Andy Huang5c5fd572012-04-08 18:19:29 -070061import com.android.ex.chips.RecipientEditTextView;
62import com.android.mail.R;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080063import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080064import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080065import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080066import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080067import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080068import com.android.mail.providers.Address;
69import com.android.mail.providers.Attachment;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080070import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080071import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070072import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080073import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080074import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070075import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070076import com.android.mail.providers.UIProvider.DraftType;
Paul Westbrook92227f62012-03-20 10:32:51 -070077import com.android.mail.utils.AccountUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080078import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080079import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080080import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080081import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080082import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080083
Mindy Pereira62de1b12012-04-06 12:17:56 -070084import org.json.JSONException;
85
Mindy Pereira8eca57a2012-03-20 16:42:34 -070086import java.io.UnsupportedEncodingException;
87import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080088import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070089import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080090import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080091import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080092import java.util.HashSet;
93import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -070094import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070095import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -080096import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080097
98public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080099 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700100 AttachmentDeletedListener, OnAccountChangedListener {
Mindy Pereira6349a042012-01-04 11:25:01 -0800101 // Identifiers for which type of composition this is
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700102 static final int COMPOSE = -1;
103 static final int REPLY = 0;
104 static final int REPLY_ALL = 1;
105 static final int FORWARD = 2;
106 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800107
108 // Integer extra holding one of the above compose action
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700109 private static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800110
Mindy Pereira326689d2012-05-17 10:14:14 -0700111 private static final String EXTRA_SHOW_CC = "showCc";
112 private static final String EXTRA_SHOW_BCC = "showBcc";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700113
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700114 private static final String UTF8_ENCODING_NAME = "UTF-8";
115
116 private static final String MAIL_TO = "mailto";
117
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700118 private static final String EXTRA_SUBJECT = "subject";
119
120 private static final String EXTRA_BODY = "body";
121
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700122 private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
123
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700124 // Extra that we can get passed from other activities
125 private static final String EXTRA_TO = "to";
126 private static final String EXTRA_CC = "cc";
127 private static final String EXTRA_BCC = "bcc";
128
129 // List of all the fields
130 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
131
Mindy Pereira82cc5662012-01-09 17:29:30 -0800132 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
133 // Map containing information about requests to create new messages, and the id of the
134 // messages that were the result of those requests.
135 //
136 // This map is used when the activity that initiated the save a of a new message, is killed
137 // before the save has completed (and when we know the id of the newly created message). When
138 // a save is completed, the service that is running in the background, will update the map
139 //
140 // When a new ComposeActivity instance is created, it will attempt to use the information in
141 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
142 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
143 // to populate the map with data stored in shared preferences.
144 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
145 // Key used to store the above map
146 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800147 /**
148 * Notifies the {@code Activity} that the caller is an Email
149 * {@code Activity}, so that the back behavior may be modified accordingly.
150 *
151 * @see #onAppUpPressed
152 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700153 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800154
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700155 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700156
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800157 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700158 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700159 // If this is an action to edit an existing draft messagge, this extra will hold the
160 // draft message
161 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800162 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800163 private static final String LOG_TAG = new LogUtils().getLogTag();
164 // Request numbers for activities we start
165 private static final int RESULT_PICK_ATTACHMENT = 1;
166 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700167 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700168 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700169
170 // Max size for attachments (5 megs). Will be overridden by account settings if found.
171 // TODO(mindyp): read this from account settings?
172 private static final int DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700173 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
174 private static final String EXTRA_REQUEST_ID = "requestId";
175 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
176 private static final String EXTRA_FOCUS_SELECTION_END = null;
177 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800178
Mindy Pereira82cc5662012-01-09 17:29:30 -0800179 /**
180 * A single thread for running tasks in the background.
181 */
182 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800183 private RecipientEditTextView mTo;
184 private RecipientEditTextView mCc;
185 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800186 private Button mCcBccButton;
187 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800188 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800189 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700190 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800191 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800192 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800193 private TextView mSubject;
194
Mindy Pereira326c6602012-01-04 15:32:42 -0800195 private ComposeModeAdapter mComposeModeAdapter;
196 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800197 private boolean mForward;
198 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800199 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700200 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800201 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800202 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800203 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800204 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800205 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800206 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800207 private boolean mTextChanged;
208 private boolean mReplyFromChanged;
209 private MenuItem mSave;
210 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800211 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800212 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800213 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800214 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
215 private Message mDraft;
216 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800217 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800218
Mindy Pereira326c6602012-01-04 15:32:42 -0800219 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700220 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
221 */
222 private boolean mLaunchedFromEmail = false;
223
224
225 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800226 * Can be called from a non-UI thread.
227 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800228 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700229 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800230 }
231
Mindy Pereira6349a042012-01-04 11:25:01 -0800232 /**
233 * Can be called from a non-UI thread.
234 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800235 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800236 launch(launcher, account, null, COMPOSE);
237 }
238
239 /**
240 * Can be called from a non-UI thread.
241 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800242 public static void reply(Context launcher, Account account, Message message) {
243 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800244 }
245
246 /**
247 * Can be called from a non-UI thread.
248 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800249 public static void replyAll(Context launcher, Account account, Message message) {
250 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800251 }
252
253 /**
254 * Can be called from a non-UI thread.
255 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800256 public static void forward(Context launcher, Account account, Message message) {
257 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800258 }
259
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800260 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800261 Intent intent = new Intent(launcher, ComposeActivity.class);
262 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
263 intent.putExtra(EXTRA_ACTION, action);
264 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700265 if (action == EDIT_DRAFT) {
266 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
267 } else {
268 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
269 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800270 launcher.startActivity(intent);
271 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800272
273 @Override
274 public void onCreate(Bundle savedInstanceState) {
275 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800276 setContentView(R.layout.compose);
277 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800278 Intent intent = getIntent();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700279 Account account = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700280 Message message;
Mindy Pereira71c9e562012-05-17 11:01:02 -0700281 boolean showQuotedText = false;
282
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700283 int action;
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700284 Object accountExtra = intent != null && intent.getExtras() != null ? intent.getExtras()
285 .get(Utils.EXTRA_ACCOUNT) : null;
286 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700287 if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE)) {
288 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
289 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
290 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
291 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
292 } else {
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700293 if (accountExtra instanceof Account) {
294 account = (Account) intent.getExtras().get(Utils.EXTRA_ACCOUNT);
295 } else if (accountExtra instanceof String) {
296 // For backwards compatibility
297 String extraAccount = intent.getStringExtra(Utils.EXTRA_ACCOUNT);
298 if (syncingAccounts.length > 0) {
299 if (!TextUtils.isEmpty(extraAccount)) {
300 for (Account a : syncingAccounts) {
301 if (a.name.equals(extraAccount)) {
302 account = a;
303 }
304 }
305 }
306 }
307 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700308 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
309 // Initialize the message from the message in the intent
310 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
311 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
312 }
Paul Westbrook92227f62012-03-20 10:32:51 -0700313 if (account == null) {
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700314 if (syncingAccounts != null && syncingAccounts.length > 0) {
315 account = syncingAccounts[0];
Paul Westbrook92227f62012-03-20 10:32:51 -0700316 }
317 }
318
319 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800320 if (mAccount == null) {
321 return;
322 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700323
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700324 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
325 mLaunchedFromEmail = true;
326 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
327 final Uri dataUri = intent.getData();
328 if (dataUri != null) {
329 final String dataScheme = intent.getData().getScheme();
330 final String accountScheme = mAccount.composeIntentUri.getScheme();
331 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
332 }
333 }
334
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700335 if (message != null && action != EDIT_DRAFT) {
336 initFromDraftMessage(message);
337 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700338 showCcBcc(savedInstanceState);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700339 showQuotedText = message.appendRefMessageContent;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700340 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700341 initFromDraftMessage(message);
Mindy Pereira326689d2012-05-17 10:14:14 -0700342 showCcBcc(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700343 // Update the action to the draft type of the previous draft
344 switch (message.draftType) {
345 case UIProvider.DraftType.REPLY:
346 action = REPLY;
347 break;
348 case UIProvider.DraftType.REPLY_ALL:
349 action = REPLY_ALL;
350 break;
351 case UIProvider.DraftType.FORWARD:
352 action = FORWARD;
353 break;
354 case UIProvider.DraftType.COMPOSE:
355 default:
356 action = COMPOSE;
357 break;
358 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700359 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700360 showQuotedText = message.appendRefMessageContent;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700361 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
362 if (mRefMessage != null) {
363 initFromRefMessage(action, mAccount.name);
Mindy Pereira326689d2012-05-17 10:14:14 -0700364 showCcBcc(mRefMessage);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700365 showQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700366 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700367 } else {
368 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700369 }
370
371 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800372 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800373 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800374 initRecipients();
Paul Westbrookf97588b2012-03-20 11:11:37 -0700375 initAttachmentsFromIntent(intent);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800376 initActionBar(action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700377 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
378 action);
Mindy Pereira75f66632012-01-11 11:42:02 -0800379 initChangeListeners();
Mindy Pereira433b1982012-04-03 11:53:07 -0700380 setFocus(action);
Mindy Pereira326689d2012-05-17 10:14:14 -0700381 updateHideOrShowCcBcc();
Mindy Pereira71c9e562012-05-17 11:01:02 -0700382 updateHideOrShowQuotedText(showQuotedText);
383 }
384
385 private void updateHideOrShowQuotedText(boolean showQuotedText) {
386 mQuotedTextView.updateCheckedState(showQuotedText);
Mindy Pereira433b1982012-04-03 11:53:07 -0700387 }
388
389 private void setFocus(int action) {
390 if (action == EDIT_DRAFT) {
391 int type = mDraft.draftType;
392 switch (type) {
393 case UIProvider.DraftType.COMPOSE:
394 case UIProvider.DraftType.FORWARD:
395 action = COMPOSE;
396 break;
397 case UIProvider.DraftType.REPLY:
398 case UIProvider.DraftType.REPLY_ALL:
399 default:
400 action = REPLY;
401 break;
402 }
403 }
404 switch (action) {
405 case FORWARD:
406 case COMPOSE:
407 mTo.requestFocus();
408 break;
409 case REPLY:
410 case REPLY_ALL:
411 default:
412 focusBody();
413 break;
414 }
415 }
416
417 /**
418 * Focus the body of the message.
419 */
420 public void focusBody() {
421 mBodyView.requestFocus();
422 int length = mBodyView.getText().length();
423
424 int signatureStartPos = getSignatureStartPosition(
425 mSignature, mBodyView.getText().toString());
426 if (signatureStartPos > -1) {
427 // In case the user deleted the newlines...
428 mBodyView.setSelection(signatureStartPos);
429 } else if (length > 0) {
430 // Move cursor to the end.
431 mBodyView.setSelection(length);
432 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800433 }
434
435 @Override
436 protected void onResume() {
437 super.onResume();
438 // Update the from spinner as other accounts
439 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800440 if (mFromSpinner != null && mAccount != null) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700441 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount);
Mindy Pereira818143e2012-01-11 13:59:49 -0800442 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800443 }
444
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800445 @Override
446 protected void onPause() {
447 super.onPause();
448
449 if (mSendConfirmDialog != null) {
450 mSendConfirmDialog.dismiss();
451 }
452 if (mRecipientErrorDialog != null) {
453 mRecipientErrorDialog.dismiss();
454 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800455 saveIfNeeded();
456 }
457
458 @Override
459 protected final void onActivityResult(int request, int result, Intent data) {
460 mAddingAttachment = false;
461
462 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
463 addAttachmentAndUpdateView(data);
464 }
465 }
466
467 @Override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700468 public final void onRestoreInstanceState(Bundle savedInstanceState) {
469 super.onRestoreInstanceState(savedInstanceState);
470 if (savedInstanceState != null) {
471 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
472 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
473 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
474 // There should be a focus and it should be an EditText since we
475 // only save these extras if these conditions are true.
476 EditText focusEditText = (EditText) getCurrentFocus();
477 final int length = focusEditText.getText().length();
478 if (selectionStart < length && selectionEnd < length) {
479 focusEditText.setSelection(selectionStart, selectionEnd);
480 }
481 }
482 }
483 }
484
485 @Override
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800486 public final void onSaveInstanceState(Bundle state) {
487 super.onSaveInstanceState(state);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700488 // The framework is happy to save and restore the selection but only if it also saves and
489 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
490 // this manually.
491 View focus = getCurrentFocus();
492 if (focus != null && focus instanceof EditText) {
493 EditText focusEditText = (EditText) focus;
494 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
495 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
496 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700497
498 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700499 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Paul Westbrook6273e962012-04-23 10:44:15 -0700500 final ReplyFromAccount selectedReplyFromAccount =
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700501 (replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
502 replyFromAccounts.get(selectedPos) :
Paul Westbrook6273e962012-04-23 10:44:15 -0700503 null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700504 if (selectedReplyFromAccount != null) {
505 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
506 .toString());
507 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
508 } else {
509 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
510 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800511
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700512 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
513 // We don't have a draft id, and we have a request id,
514 // save the request id.
515 state.putInt(EXTRA_REQUEST_ID, mRequestId);
516 }
517
518 // We want to restore the current mode after a pause
519 // or rotation.
520 int mode = getMode();
521 state.putInt(EXTRA_ACTION, mode);
522
523 Message message = createMessage(selectedReplyFromAccount, mode);
524 state.putParcelable(EXTRA_MESSAGE, message);
525
526 if (mRefMessage != null) {
527 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
528 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700529 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
530 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700531 }
532
533 private int getMode() {
534 int mode = ComposeActivity.COMPOSE;
535 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700536 if (actionBar != null
537 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700538 mode = actionBar.getSelectedNavigationIndex();
539 }
540 return mode;
541 }
542
543 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
544 Message message = new Message();
545 message.id = UIProvider.INVALID_MESSAGE_ID;
546 message.serverId =UIProvider.INVALID_MESSAGE_ID;
547 message.uri = null;
548 message.conversationUri = null;
549 message.subject = mSubject.getText().toString();
550 message.snippet = null;
Paul Westbrook91906812012-04-25 11:03:27 -0700551 message.from = selectedReplyFromAccount != null ?
552 selectedReplyFromAccount.name : mAccount.name;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700553 message.to = mTo.getText().toString();
Mindy Pereira4b1377e2012-04-18 15:08:05 -0700554 message.cc = mCc.getText().toString();
555 message.bcc = mBcc.getText().toString();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700556 message.replyTo = null;
557 message.dateReceivedMs = 0;
558 String htmlBody = Html.toHtml(mBodyView.getText());
559 StringBuilder fullBody = new StringBuilder(htmlBody);
560 message.bodyHtml = fullBody.toString();
561 message.bodyText = mBodyView.getText().toString();
562 message.embedsExternalResources = false;
563 message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
Mindy Pereirad2bef8b2012-05-30 12:14:52 -0700564 message.draftType = getDraftType(mode);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700565 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
566 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
567 message.hasAttachments = attachments != null && attachments.size() > 0;
568 message.attachmentListUri = null;
569 message.messageFlags = 0;
570 message.saveUri = null;
571 message.sendUri = null;
572 message.alwaysShowImages = false;
573 message.attachmentsJson = Attachment.toJSONArray(attachments);
574 CharSequence quotedText = mQuotedTextView.getQuotedText();
575 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
576 .getQuotedTextOffset(quotedText.toString()) : -1;
577 message.accountUri = null;
578 return message;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800579 }
580
Mindy Pereira818143e2012-01-11 13:59:49 -0800581 @VisibleForTesting
582 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700583 if (account == null) {
584 return;
585 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700586 if (!account.equals(mAccount)) {
587 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700588 mCachedSettings = mAccount.settings;
589 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700590 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800591 }
592
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700593 private void initFromSpinner(Bundle bundle, int action) {
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700594 String accountString = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700595 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700596 action = COMPOSE;
597 }
598 mFromSpinner.asyncInitFromSpinner(action, mAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700599 if (bundle != null) {
600 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
601 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
602 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
603 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
604 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
605 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
606 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700607 }
608 if (mReplyFromAccount == null) {
609 if (mDraft != null) {
610 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
611 } else if (mRefMessage != null) {
612 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
613 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700614 }
615 if (mReplyFromAccount == null) {
616 mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700617 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700618 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700619
Mindy Pereira62de1b12012-04-06 12:17:56 -0700620 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700621
Mindy Pereira62de1b12012-04-06 12:17:56 -0700622 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700623 // If there is only 1 account, just show that account.
624 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700625 // send mail from / save drafts to.
626 mFromStatic.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700627 mFromStaticText.setText(mAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700628 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700629 } else {
630 mFromStatic.setVisibility(View.VISIBLE);
631 mFromStaticText.setText(mAccount.name);
632 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700633 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800634 }
635
Mindy Pereira62de1b12012-04-06 12:17:56 -0700636 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
637 if (refMessage.accountUri != null) {
638 // This must be from combined inbox.
639 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
640 for (ReplyFromAccount from : replyFromAccounts) {
641 if (from.account.uri.equals(refMessage.accountUri)) {
642 return from;
643 }
644 }
645 return null;
646 } else {
647 return getReplyFromAccount(account, refMessage);
648 }
649 }
650
651 /**
652 * Given an account and which email address the message was sent to,
653 * return who the message should be sent from.
654 * @param account Account in which the message arrived.
655 * @param sentTo Email address to which the message was sent.
656 * @return the address from which to reply.
657 */
658 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
659 // First see if we are supposed to use the default address or
660 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -0700661 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700662 return getDefaultReplyFromAccount(account);
663 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -0700664 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -0700665 // all the message recipients and find one that matches
666 // a custom from or account.
667 List<String> allRecipients = new ArrayList<String>();
668 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.to)));
669 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.cc)));
670 return getMatchingRecipient(account, allRecipients);
671 }
672 }
673
674 /**
675 * Compare all the recipients of an email to the current account and all
676 * custom addresses associated with that account. Return the match if there
677 * is one, or the default account if there isn't.
678 */
679 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
680 // Tokenize the list and place in a hashmap.
681 ReplyFromAccount matchingReplyFrom = null;
682 Rfc822Token[] tokens;
683 HashSet<String> recipientsMap = new HashSet<String>();
684 for (String address : sentTo) {
685 tokens = Rfc822Tokenizer.tokenize(address);
686 for (int i = 0; i < tokens.length; i++) {
687 recipientsMap.add(tokens[i].getAddress());
688 }
689 }
690
691 int matchingAddressCount = 0;
692 List<ReplyFromAccount> customFroms;
693 try {
694 customFroms = FromAddressSpinner.getAccountSpecificFroms(account);
695 if (customFroms != null) {
696 for (ReplyFromAccount entry : customFroms) {
697 if (recipientsMap.contains(entry.address)) {
698 matchingReplyFrom = entry;
699 matchingAddressCount++;
700 }
701 }
702 }
703 } catch (JSONException e) {
704 LogUtils.wtf(LOG_TAG, "Exception parsing from addresses for account %s",
705 account.name);
706 }
707 if (matchingAddressCount > 1) {
708 matchingReplyFrom = getDefaultReplyFromAccount(account);
709 }
710 return matchingReplyFrom;
711 }
712
713 private ReplyFromAccount getDefaultReplyFromAccount(Account account) {
714 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
715 for (ReplyFromAccount from : replyFromAccounts) {
716 if (from.isDefault) {
717 return from;
718 }
719 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700720 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
721 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700722 }
723
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700724 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
725 String sender = msg.from;
Mindy Pereira62de1b12012-04-06 12:17:56 -0700726 ReplyFromAccount replyFromAccount = null;
727 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
728 if (TextUtils.equals(account.name, sender)) {
729 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700730 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700731 } else {
732 for (ReplyFromAccount fromAccount : replyFromAccounts) {
733 if (TextUtils.equals(fromAccount.name, sender)) {
734 replyFromAccount = fromAccount;
735 break;
736 }
737 }
738 }
739 return replyFromAccount;
740 }
741
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800742 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800743 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800744 if (mCcBccButton != null) {
745 mCcBccButton.setOnClickListener(this);
746 }
747 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800748 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800749 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
750 if (mAttachmentsButton != null) {
751 mAttachmentsButton.setOnClickListener(this);
752 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800753 mTo = (RecipientEditTextView) findViewById(R.id.to);
754 mCc = (RecipientEditTextView) findViewById(R.id.cc);
755 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800756 // TODO: add special chips text change watchers before adding
757 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800758 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800759 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
760 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -0700761 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800762 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800763 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800764 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800765 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800766 }
767
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700768 protected TextView getBody() {
769 return mBodyView;
770 }
771
772 @VisibleForTesting
773 public Account getFromAccount() {
774 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
775 mReplyFromAccount.account : mAccount;
776 }
777
Mindy Pereira75f66632012-01-11 11:42:02 -0800778 // Now that the message has been initialized from any existing draft or
779 // ref message data, set up listeners for any changes that occur to the
780 // message.
781 private void initChangeListeners() {
782 mSubject.addTextChangedListener(this);
783 mBodyView.addTextChangedListener(this);
784 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
785 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
786 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
787 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800788 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800789 }
790
Mindy Pereira326c6602012-01-04 15:32:42 -0800791 private void initActionBar(int action) {
792 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800793 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700794 if (actionBar == null) {
795 return;
796 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800797 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800798 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
799 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800800 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800801 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800802 if (mComposeModeAdapter == null) {
803 mComposeModeAdapter = new ComposeModeAdapter(this);
804 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800805 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
806 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800807 switch (action) {
808 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800809 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800810 break;
811 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800812 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800813 break;
814 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800815 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800816 break;
817 }
818 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700819 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
820 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
821 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800822 }
823
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800824 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700825 setSubject(mRefMessage, action);
826 // Setup recipients
827 if (action == FORWARD) {
828 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800829 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700830 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700831 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700832 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
833 initAttachments(mRefMessage);
834 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800835 }
836
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700837 private void initFromDraftMessage(Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700838 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
839
840 mDraft = message;
841 mDraftId = message.id;
842 mSubject.setText(message.subject);
843 mForward = message.draftType == UIProvider.DraftType.FORWARD;
844 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
845 addToAddresses(toAddresses);
846 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
847 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -0700848 if (message.hasAttachments) {
849 List<Attachment> attachments = message.getAttachments();
850 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -0700851 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -0700852 }
853 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -0700854 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -0700855 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700856 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -0700857 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700858 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira002ff522012-05-30 10:31:26 -0700859 CharSequence htmlText = Html.fromHtml(message.bodyHtml);
860 if (quotedTextIndex > -1) {
861 htmlText = htmlText.subSequence(0, quotedTextIndex);
862 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
863 message.bodyHtml.length());
864 }
865 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700866 } else {
Mindy Pereira002ff522012-05-30 10:31:26 -0700867 CharSequence bodyText = quotedTextIndex > -1 ?
868 message.bodyText.substring(0, quotedTextIndex) : message.bodyText;
869 if (quotedTextIndex > -1) {
870 quotedText = message.bodyText.substring(quotedTextIndex);
871 }
872 mBodyView.setText(bodyText);
873 }
874 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -0700875 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700876 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700877 }
878
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700879 /**
880 * Fill all the widgets with the content found in the Intent Extra, if any.
881 * Also apply the same style to all widgets. Note: if initFromExtras is
882 * called as a result of switching between reply, reply all, and forward per
883 * the latest revision of Gmail, and the user has already made changes to
884 * attachments on a previous incarnation of the message (as a reply, reply
885 * all, or forward), the original attachments from the message will not be
886 * re-instantiated. The user's changes will be respected. This follows the
887 * web gmail interaction.
888 */
889 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700890 // If we were invoked with a SENDTO intent, the value
891 // should take precedence
892 final Uri dataUri = intent.getData();
893 if (dataUri != null) {
894 if (MAIL_TO.equals(dataUri.getScheme())) {
895 initFromMailTo(dataUri.toString());
896 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -0700897 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700898 String toText = dataUri.getSchemeSpecificPart();
899 if (toText != null) {
900 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -0700901 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700902 }
903 }
904 }
905 }
906
907 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
908 if (extraStrings != null) {
909 addToAddresses(Arrays.asList(extraStrings));
910 }
911 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
912 if (extraStrings != null) {
913 addCcAddresses(Arrays.asList(extraStrings), null);
914 }
915 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
916 if (extraStrings != null) {
917 addBccAddresses(Arrays.asList(extraStrings));
918 }
919
920 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
921 if (extraString != null) {
922 mSubject.setText(extraString);
923 }
924
925 for (String extra : ALL_EXTRAS) {
926 if (intent.hasExtra(extra)) {
927 String value = intent.getStringExtra(extra);
928 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700929 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700930 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700931 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700932 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700933 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700934 } else if (EXTRA_SUBJECT.equals(extra)) {
935 mSubject.setText(value);
936 } else if (EXTRA_BODY.equals(extra)) {
937 setBody(value, true /* with signature */);
938 }
939 }
940 }
941
942 Bundle extras = intent.getExtras();
943 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700944 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
945 if (text != null) {
946 setBody(text, true /* with signature */);
947 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700948 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700949 }
950
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700951 @VisibleForTesting
952 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -0700953 // TODO: handle the case where there are spaces in the display name as
954 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
955 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700956 // Since URLDecode.decode changes + into ' ', and + is a valid
957 // email character, we need to find/ replace these ourselves before
958 // decoding.
959 String replacePlus = s.replace("+", "%2B");
Mindy Pereiraa4069f22012-05-30 15:31:45 -0700960 try {
961 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
962 } catch (IllegalArgumentException e) {
963 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
964 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
965 } else {
966 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
967 }
968 return null;
969 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700970 }
971
972 /**
973 * Initialize the compose view from a String representing a mailTo uri.
974 * @param mailToString The uri as a string.
975 */
976 public void initFromMailTo(String mailToString) {
977 // We need to disguise this string as a URI in order to parse it
978 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
979 Uri uri = Uri.parse("foo://" + mailToString);
980 int index = mailToString.indexOf("?");
981 int length = "mailto".length() + 1;
982 String to;
983 try {
984 // Extract the recipient after mailto:
985 if (index == -1) {
986 to = decodeEmailInUri(mailToString.substring(length));
987 } else {
988 to = decodeEmailInUri(mailToString.substring(length, index));
989 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -0700990 if (!TextUtils.isEmpty(to)) {
991 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
992 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700993 } catch (UnsupportedEncodingException e) {
994 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
995 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
996 } else {
997 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
998 }
999 }
1000
1001 List<String> cc = uri.getQueryParameters("cc");
1002 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1003
1004 List<String> otherTo = uri.getQueryParameters("to");
1005 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1006
1007 List<String> bcc = uri.getQueryParameters("bcc");
1008 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1009
1010 List<String> subject = uri.getQueryParameters("subject");
1011 if (subject.size() > 0) {
1012 try {
1013 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
1014 } catch (UnsupportedEncodingException e) {
1015 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1016 e.getMessage(), subject);
1017 }
1018 }
1019
1020 List<String> body = uri.getQueryParameters("body");
1021 if (body.size() > 0) {
1022 try {
1023 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
1024 true /* with signature */);
1025 } catch (UnsupportedEncodingException e) {
1026 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1027 }
1028 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001029 }
1030
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001031 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -08001032 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001033 }
1034
Paul Westbrookf97588b2012-03-20 11:11:37 -07001035 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001036 Bundle extras = intent.getExtras();
1037 if (extras == null) {
1038 extras = Bundle.EMPTY;
1039 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001040 final String action = intent.getAction();
1041 if (!mAttachmentsChanged) {
1042 long totalSize = 0;
1043 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1044 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1045 for (String uriString : uris) {
1046 final Uri uri = Uri.parse(uriString);
1047 long size = 0;
1048 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001049 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001050 } catch (AttachmentFailureException e) {
1051 // A toast has already been shown to the user,
1052 // just break out of the loop.
1053 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1054 }
1055 totalSize += size;
1056 }
1057 }
1058 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
1059 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1060 long size = 0;
1061 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001062 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001063 } catch (AttachmentFailureException e) {
1064 // A toast has already been shown to the user, so just
1065 // exit.
1066 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1067 }
1068 totalSize += size;
1069 }
1070
1071 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
1072 && extras.containsKey(Intent.EXTRA_STREAM)) {
1073 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
1074 for (Parcelable uri : uris) {
1075 long size = 0;
1076 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001077 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001078 } catch (AttachmentFailureException e) {
1079 // A toast has already been shown to the user,
1080 // just break out of the loop.
1081 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1082 }
1083 totalSize += size;
1084 }
1085 }
1086
1087 if (totalSize > 0) {
1088 mAttachmentsChanged = true;
1089 updateSaveUi();
1090 }
1091 }
1092 }
1093
1094
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001095 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1096 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001097 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1098 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001099 }
1100
1101 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001102 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001103 boolean ccVisible = mCcBccView.isCcVisible();
1104 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001105 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001106 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001107 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001108 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001109 : R.string.add_bcc_label));
1110 } else {
1111 mCcBccButton.setVisibility(View.GONE);
1112 }
1113 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001114 }
1115
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001116 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001117 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1118 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1119 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1120 if (showCc || showBcc) {
1121 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001122 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001123 }
1124 }
1125
Mindy Pereira326689d2012-05-17 10:14:14 -07001126 private void showCcBcc(Message refMessage) {
1127 if (refMessage != null) {
1128 boolean showCc = !TextUtils.isEmpty(refMessage.cc);
1129 boolean showBcc = !TextUtils.isEmpty(refMessage.bcc);
1130 if (showCc || showBcc) {
1131 mCcBccView.show(false, showCc, showBcc);
1132 }
1133 }
1134 updateHideOrShowCcBcc();
1135 }
1136
Mindy Pereira013194c2012-01-06 15:09:33 -08001137 /**
1138 * Add attachment and update the compose area appropriately.
1139 * @param data
1140 */
1141 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001142 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1143 }
1144
Andy Huang5c5fd572012-04-08 18:19:29 -07001145 public void addAttachmentAndUpdateView(Uri contentUri) {
1146 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001147 return;
1148 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001149 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001150 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1151 } catch (AttachmentFailureException e) {
1152 // A toast has already been shown to the user, no need to do
1153 // anything.
1154 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1155 }
1156 }
1157
1158 public void addAttachmentAndUpdateView(Attachment attachment) {
1159 try {
1160 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001161 if (size > 0) {
1162 mAttachmentsChanged = true;
1163 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001164 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001165 } catch (AttachmentFailureException e) {
1166 // A toast has already been shown to the user, no need to do
1167 // anything.
1168 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -08001169 }
1170 }
1171
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001172 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001173 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001174 // Don't populate the address if this is a forward.
1175 if (action == ComposeActivity.FORWARD) {
1176 return;
1177 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001178 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001179 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001180
Mindy Pereira818143e2012-01-11 13:59:49 -08001181 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001182 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001183 // This is the email address of the current user, i.e. the one composing
1184 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001185 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001186 String fromAddress = refMessage.from;
1187 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
1188 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001189 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001190
1191 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1192 // Cc list is the union of the To and Cc recipients of the original
1193 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001194 // already on the To list.
1195 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001196 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1197 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001198 addToAddresses(toAddresses);
1199 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001200 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001201 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1202 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -08001203 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001204 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001205 addRecipients(accountEmail, ccAddresses,
1206 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001207 addCcAddresses(ccAddresses, toAddresses);
1208 }
1209 }
1210
1211 private void addToAddresses(Collection<String> addresses) {
1212 addAddressesToList(addresses, mTo);
1213 }
1214
1215 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001216 addCcAddressesToList(tokenizeAddressList(addresses),
1217 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001218 }
1219
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001220 private void addBccAddresses(Collection<String> addresses) {
1221 addAddressesToList(addresses, mBcc);
1222 }
1223
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001224 @VisibleForTesting
1225 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1226 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1227 String address;
1228
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001229 if (compareToList == null) {
1230 for (Rfc822Token[] tokens : addresses) {
1231 for (int i = 0; i < tokens.length; i++) {
1232 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001233 list.append(address + END_TOKEN);
1234 }
1235 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001236 } else {
1237 HashSet<String> compareTo = convertToHashSet(compareToList);
1238 for (Rfc822Token[] tokens : addresses) {
1239 for (int i = 0; i < tokens.length; i++) {
1240 address = tokens[i].toString();
1241 // Check if this is a duplicate:
1242 if (!compareTo.contains(tokens[i].getAddress())) {
1243 // Get the address here
1244 list.append(address + END_TOKEN);
1245 }
1246 }
1247 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001248 }
1249 }
1250
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001251 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
1252 HashSet<String> hash = new HashSet<String>();
1253 for (Rfc822Token[] tokens : list) {
1254 for (int i = 0; i < tokens.length; i++) {
1255 hash.add(tokens[i].getAddress());
1256 }
1257 }
1258 return hash;
1259 }
1260
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001261 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1262 @VisibleForTesting
1263 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1264
1265 for (String address: addresses) {
1266 tokenized.add(Rfc822Tokenizer.tokenize(address));
1267 }
1268 return tokenized;
1269 }
1270
1271 @VisibleForTesting
1272 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1273 for (String address : addresses) {
1274 addAddressToList(address, list);
1275 }
1276 }
1277
1278 private void addAddressToList(String address, RecipientEditTextView list) {
1279 if (address == null || list == null)
1280 return;
1281
1282 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1283
1284 for (int i = 0; i < tokens.length; i++) {
1285 list.append(tokens[i] + END_TOKEN);
1286 }
1287 }
1288
1289 @VisibleForTesting
1290 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001291 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001292 // The To recipient is the reply-to address specified in the original
1293 // message, unless it is:
1294 // the current user OR a custom from of the current user, in which case
1295 // it's the To recipient list of the original message.
1296 // OR missing, in which case use the sender of the original message
1297 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001298 if (!TextUtils.isEmpty(replyToAddress)) {
1299 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001300 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001301 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001302 }
1303 return toAddresses;
1304 }
1305
1306 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
1307 for (String email : addresses) {
1308 // Do not add this account, or any of the custom froms, to the list
1309 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001310 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001311 if (!account.equalsIgnoreCase(recipientAddress)) {
1312 recipients.add(email.replace("\"\"", ""));
1313 }
1314 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001315 }
1316
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001317 private void setSubject(Message refMessage, int action) {
1318 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001319 String prefix;
1320 String correctedSubject = null;
1321 if (action == ComposeActivity.COMPOSE) {
1322 prefix = "";
1323 } else if (action == ComposeActivity.FORWARD) {
1324 prefix = getString(R.string.forward_subject_label);
1325 } else {
1326 prefix = getString(R.string.reply_subject_label);
1327 }
1328
1329 // Don't duplicate the prefix
1330 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
1331 correctedSubject = subject;
1332 } else {
1333 correctedSubject = String
1334 .format(getString(R.string.formatted_subject), prefix, subject);
1335 }
1336 mSubject.setText(correctedSubject);
1337 }
1338
Mindy Pereira818143e2012-01-11 13:59:49 -08001339 private void initRecipients() {
1340 setupRecipients(mTo);
1341 setupRecipients(mCc);
1342 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001343 }
1344
Mindy Pereira818143e2012-01-11 13:59:49 -08001345 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001346 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -08001347 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001348 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001349 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001350 int offset = accountName.indexOf("@") + 1;
1351 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001352 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001353 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001354 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001355 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001356 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001357 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001358 }
1359
1360 @Override
1361 public void onClick(View v) {
1362 int id = v.getId();
1363 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001364 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001365 // Verify that cc/ bcc aren't showing.
1366 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001367 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001368 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001369 case R.id.add_attachment:
1370 doAttach();
1371 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001372 }
1373 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001374
1375 @Override
1376 public boolean onCreateOptionsMenu(Menu menu) {
1377 super.onCreateOptionsMenu(menu);
1378 MenuInflater inflater = getMenuInflater();
1379 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001380 mSave = menu.findItem(R.id.save);
1381 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001382 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1383 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1384 if (helpItem != null) {
1385 helpItem.setVisible(mAccount != null
1386 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1387 }
1388 if (sendFeedbackItem != null) {
1389 sendFeedbackItem.setVisible(mAccount != null
1390 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1391 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001392 return true;
1393 }
1394
1395 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001396 public boolean onPrepareOptionsMenu(Menu menu) {
1397 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001398 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001399 // Its possible there is a menu item OR a button.
1400 boolean ccFieldVisible = mCc.isShown();
1401 boolean bccFieldVisible = mBcc.isShown();
1402 if (!ccFieldVisible || !bccFieldVisible) {
1403 ccBcc.setVisible(true);
1404 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1405 : R.string.add_bcc_label));
1406 } else {
1407 ccBcc.setVisible(false);
1408 }
1409 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001410 if (mSave != null) {
1411 mSave.setEnabled(shouldSave());
1412 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001413 return true;
1414 }
1415
1416 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001417 public boolean onOptionsItemSelected(MenuItem item) {
1418 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001419 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001420 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001421 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -08001422 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001423 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001424 case R.id.add_cc_bcc:
1425 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001426 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001427 case R.id.save:
Mindy Pereira48e31b02012-05-30 13:12:24 -07001428 doSave(true);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001429 break;
1430 case R.id.send:
1431 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001432 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001433 case R.id.discard:
1434 doDiscard();
1435 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001436 case R.id.settings:
1437 Utils.showSettings(this, mAccount);
1438 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001439 case android.R.id.home:
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001440 onAppUpPressed();
Mindy Pereirafbe40192012-03-20 10:40:45 -07001441 break;
1442 case R.id.help_info_menu_item:
1443 // TODO: enable context sensitive help
Paul Westbrook498e76d2012-04-12 16:33:02 -07001444 Utils.showHelp(this, mAccount, null);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001445 break;
1446 case R.id.feedback_menu_item:
1447 Utils.sendFeedback(this, mAccount);
1448 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001449 default:
1450 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001451 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001452 }
1453 return !handled ? super.onOptionsItemSelected(item) : handled;
1454 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001455
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001456 private void onAppUpPressed() {
1457 if (mLaunchedFromEmail) {
1458 // If this was started from Gmail, simply treat app up as the system back button, so
1459 // that the last view is restored.
1460 onBackPressed();
1461 return;
1462 }
1463
1464 // Fire the main activity to ensure it launches the "top" screen of mail.
1465 // Since the main Activity is singleTask, it should revive that task if it was already
1466 // started.
1467 final Intent mailIntent =
1468 Utils.createViewFolderIntent(mAccount.settings.defaultInbox, mAccount, null, false);
1469
1470 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
1471 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
1472 startActivity(mailIntent);
1473 finish();
1474 }
1475
Mindy Pereira33fe9082012-01-09 16:24:30 -08001476 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001477 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001478 }
1479
Mindy Pereira48e31b02012-05-30 13:12:24 -07001480 private void doSave(boolean showToast) {
1481 // Clear the IME composing suggestions from the body and subject before saving.
1482 clearImeText(mBodyView);
1483 clearImeText(mSubject);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001484 sendOrSaveWithSanityChecks(true, showToast, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07001485 }
1486
1487 private void clearImeText(TextView v) {
1488 v.clearComposingText();
1489 BaseInputConnection.removeComposingSpans(v.getEditableText());
Mindy Pereira33fe9082012-01-09 16:24:30 -08001490 }
1491
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001492 @VisibleForTesting
1493 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001494 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001495 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1496 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001497 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1498 }
1499
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001500 @VisibleForTesting
1501 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001502 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001503 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001504 public final SendOrSaveCallback mSendOrSaveCallback;
1505 @VisibleForTesting
1506 public final SendOrSaveMessage mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001507
1508 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1509 SendOrSaveCallback callback) {
1510 mContext = context;
1511 mSendOrSaveCallback = callback;
1512 mSendOrSaveMessage = message;
1513 }
1514
1515 @Override
1516 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001517 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001518
Mindy Pereira92551d02012-04-05 11:31:12 -07001519 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001520 Message message = mSendOrSaveCallback.getMessage();
1521 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001522 // If a previous draft has been saved, in an account that is different
1523 // than what the user wants to send from, remove the old draft, and treat this
1524 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001525 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001526 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1527 ContentResolver resolver = mContext.getContentResolver();
1528 ContentValues values = new ContentValues();
1529 values.put(BaseColumns._ID, messageId);
Mindy Pereira92551d02012-04-05 11:31:12 -07001530 if (selectedAccount.account.expungeMessageUri != null) {
1531 resolver.update(selectedAccount.account.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001532 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001533 } else {
1534 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001535 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001536 // reset messageId to 0, so a new message will be created
1537 messageId = UIProvider.INVALID_MESSAGE_ID;
1538 }
1539 }
1540
1541 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001542 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001543 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001544 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001545 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1546 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001547 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001548 ContentResolver resolver = mContext.getContentResolver();
Mindy Pereira92551d02012-04-05 11:31:12 -07001549 Uri messageUri = resolver
1550 .insert(sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
1551 : selectedAccount.account.sendMessageUri,
1552 sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001553 if (sendOrSaveMessage.mSave && messageUri != null) {
1554 Cursor messageCursor = resolver.query(messageUri,
1555 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001556 if (messageCursor != null) {
1557 try {
1558 if (messageCursor.moveToFirst()) {
1559 // Broadcast notification that a new message has
1560 // been allocated
1561 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1562 new Message(messageCursor));
1563 }
1564 } finally {
1565 messageCursor.close();
1566 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001567 }
1568 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001569 }
1570
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001571 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001572 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001573 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001574 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001575 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001576 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001577 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001578 }
1579 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1580 }
1581 }
1582
1583 // Array of the outstanding send or save tasks. Access is synchronized
1584 // with the object itself
1585 /* package for testing */
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001586 @VisibleForTesting
1587 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001588 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001589 private String mSignature;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001590
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001591 @VisibleForTesting
1592 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07001593 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001594 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001595 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001596 @VisibleForTesting
1597 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001598 final int mRequestId;
1599
Mindy Pereira92551d02012-04-05 11:31:12 -07001600 public SendOrSaveMessage(ReplyFromAccount account, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001601 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001602 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001603 mValues = values;
1604 mRefMessageId = refMessageId;
1605 mSave = save;
1606 mRequestId = mValues.hashCode() ^ hashCode();
1607 }
1608
1609 int requestId() {
1610 return mRequestId;
1611 }
1612 }
1613
1614 /**
1615 * Get the to recipients.
1616 */
1617 public String[] getToAddresses() {
1618 return getAddressesFromList(mTo);
1619 }
1620
1621 /**
1622 * Get the cc recipients.
1623 */
1624 public String[] getCcAddresses() {
1625 return getAddressesFromList(mCc);
1626 }
1627
1628 /**
1629 * Get the bcc recipients.
1630 */
1631 public String[] getBccAddresses() {
1632 return getAddressesFromList(mBcc);
1633 }
1634
1635 public String[] getAddressesFromList(RecipientEditTextView list) {
1636 if (list == null) {
1637 return new String[0];
1638 }
1639 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1640 int count = tokens.length;
1641 String[] result = new String[count];
1642 for (int i = 0; i < count; i++) {
1643 result[i] = tokens[i].toString();
1644 }
1645 return result;
1646 }
1647
1648 /**
1649 * Check for invalid email addresses.
1650 * @param to String array of email addresses to check.
1651 * @param wrongEmailsOut Emails addresses that were invalid.
1652 */
1653 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
1654 for (String email : to) {
1655 if (!mValidator.isValid(email)) {
1656 wrongEmailsOut.add(email);
1657 }
1658 }
1659 }
1660
1661 /**
1662 * Show an error because the user has entered an invalid recipient.
1663 * @param message
1664 */
1665 public void showRecipientErrorDialog(String message) {
1666 // Only 1 invalid recipients error dialog should be allowed up at a
1667 // time.
1668 if (mRecipientErrorDialog != null) {
1669 mRecipientErrorDialog.dismiss();
1670 }
1671 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1672 R.string.recipient_error_dialog_title)
1673 .setIconAttribute(android.R.attr.alertDialogIcon)
1674 .setCancelable(false)
1675 .setPositiveButton(
1676 R.string.ok, new Dialog.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001677 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08001678 public void onClick(DialogInterface dialog, int which) {
1679 // after the user dismisses the recipient error
1680 // dialog we want to make sure to refocus the
1681 // recipient to field so they can fix the issue
1682 // easily
1683 if (mTo != null) {
1684 mTo.requestFocus();
1685 }
1686 mRecipientErrorDialog = null;
1687 }
1688 }).show();
1689 }
1690
1691 /**
1692 * Update the state of the UI based on whether or not the current draft
1693 * needs to be saved and the message is not empty.
1694 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001695 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001696 if (mSave != null) {
1697 mSave.setEnabled((shouldSave() && !isBlank()));
1698 }
1699 }
1700
1701 /**
1702 * Returns true if we need to save the current draft.
1703 */
1704 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001705 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001706 // The message should only be saved if:
1707 // It hasn't been sent AND
1708 // Some text has been added to the message OR
1709 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001710 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -08001711 (mReplyFromChanged && !isBlank()));
1712 }
1713 }
1714
1715 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001716 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001717 * @return boolean
1718 */
1719 public boolean isBlank() {
1720 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001721 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1722 mBodyView.getText().toString()) == 0)
1723 && mTo.length() == 0
1724 && mCc.length() == 0 && mBcc.length() == 0
1725 && mAttachmentsView.getAttachments().size() == 0;
1726 }
1727
1728 @VisibleForTesting
1729 protected int getSignatureStartPosition(String signature, String bodyText) {
1730 int startPos = -1;
1731
1732 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1733 return startPos;
1734 }
1735
1736 int bodyLength = bodyText.length();
1737 int signatureLength = signature.length();
1738 String printableVersion = convertToPrintableSignature(signature);
1739 int printableLength = printableVersion.length();
1740
1741 if (bodyLength >= printableLength
1742 && bodyText.substring(bodyLength - printableLength)
1743 .equals(printableVersion)) {
1744 startPos = bodyLength - printableLength;
1745 } else if (bodyLength >= signatureLength
1746 && bodyText.substring(bodyLength - signatureLength)
1747 .equals(signature)) {
1748 startPos = bodyLength - signatureLength;
1749 }
1750 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001751 }
1752
1753 /**
1754 * Allows any changes made by the user to be ignored. Called when the user
1755 * decides to discard a draft.
1756 */
1757 private void discardChanges() {
1758 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001759 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001760 mReplyFromChanged = false;
1761 }
1762
1763 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001764 * @param body
1765 * @param save
1766 * @param showToast
1767 * @return Whether the send or save succeeded.
1768 */
1769 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1770 final boolean orientationChanged) {
1771 String[] to, cc, bcc;
1772 Editable body = mBodyView.getEditableText();
Mindy Pereira181df782012-03-01 13:32:44 -08001773 if (orientationChanged) {
1774 to = cc = bcc = new String[0];
1775 } else {
1776 to = getToAddresses();
1777 cc = getCcAddresses();
1778 bcc = getBccAddresses();
1779 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001780
Mindy Pereira181df782012-03-01 13:32:44 -08001781 // Don't let the user send to nobody (but it's okay to save a message
1782 // with no recipients)
1783 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1784 showRecipientErrorDialog(getString(R.string.recipient_needed));
1785 return false;
1786 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001787
Mindy Pereira181df782012-03-01 13:32:44 -08001788 List<String> wrongEmails = new ArrayList<String>();
1789 if (!save) {
1790 checkInvalidEmails(to, wrongEmails);
1791 checkInvalidEmails(cc, wrongEmails);
1792 checkInvalidEmails(bcc, wrongEmails);
1793 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001794
Mindy Pereira181df782012-03-01 13:32:44 -08001795 // Don't let the user send an email with invalid recipients
1796 if (wrongEmails.size() > 0) {
1797 String errorText = String.format(getString(R.string.invalid_recipient),
1798 wrongEmails.get(0));
1799 showRecipientErrorDialog(errorText);
1800 return false;
1801 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001802
Mindy Pereira181df782012-03-01 13:32:44 -08001803 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001804 @Override
Mindy Pereira181df782012-03-01 13:32:44 -08001805 public void onClick(DialogInterface dialog, int which) {
1806 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1807 }
1808 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001809
Mindy Pereira181df782012-03-01 13:32:44 -08001810 // Show a warning before sending only if there are no attachments.
1811 if (!save) {
1812 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1813 boolean warnAboutEmptySubject = isSubjectEmpty();
1814 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001815
Mindy Pereira181df782012-03-01 13:32:44 -08001816 // A warning about an empty body may not be warranted when
1817 // forwarding mails, since a common use case is to forward
1818 // quoted text and not append any more text.
1819 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001820
Mindy Pereira181df782012-03-01 13:32:44 -08001821 // When we bring up a dialog warning the user about a send,
1822 // assume that they accept sending the message. If they do not,
1823 // the dialog listener is required to enable sending again.
1824 if (warnAboutEmptySubject) {
1825 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1826 return true;
1827 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001828
Mindy Pereira181df782012-03-01 13:32:44 -08001829 if (warnAboutEmptyBody) {
1830 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1831 return true;
1832 }
1833 }
1834 // Ask for confirmation to send (if always required)
1835 if (showSendConfirmation()) {
1836 showSendConfirmDialog(R.string.confirm_send_message, listener);
1837 return true;
1838 }
1839 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001840
Mindy Pereira181df782012-03-01 13:32:44 -08001841 sendOrSave(body, save, showToast, false);
1842 return true;
1843 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001844
Mindy Pereira181df782012-03-01 13:32:44 -08001845 /**
1846 * Returns a boolean indicating whether warnings should be shown for empty
1847 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07001848 *
Mindy Pereira181df782012-03-01 13:32:44 -08001849 * @return True if a warning should be shown for empty text fields
1850 */
1851 protected boolean showEmptyTextWarnings() {
1852 return mAttachmentsView.getAttachments().size() == 0;
1853 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001854
Mindy Pereira181df782012-03-01 13:32:44 -08001855 /**
1856 * Returns a boolean indicating whether the user should confirm each send
1857 *
1858 * @return True if a warning should be on each send
1859 */
1860 protected boolean showSendConfirmation() {
1861 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1862 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001863
Mindy Pereira181df782012-03-01 13:32:44 -08001864 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1865 if (mSendConfirmDialog != null) {
1866 mSendConfirmDialog.dismiss();
1867 mSendConfirmDialog = null;
1868 }
1869 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1870 .setTitle(R.string.confirm_send_title)
1871 .setIconAttribute(android.R.attr.alertDialogIcon)
1872 .setPositiveButton(R.string.send, listener)
1873 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1874 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001875
Mindy Pereira181df782012-03-01 13:32:44 -08001876 /**
1877 * Returns whether the ComposeArea believes there is any text in the body of
1878 * the composition. TODO: When ComposeArea controls the Body as well, add
1879 * that here.
1880 */
1881 public boolean isBodyEmpty() {
1882 return !mQuotedTextView.isTextIncluded();
1883 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001884
Mindy Pereira181df782012-03-01 13:32:44 -08001885 /**
1886 * Test to see if the subject is empty.
1887 *
1888 * @return boolean.
1889 */
1890 // TODO: this will likely go away when composeArea.focus() is implemented
1891 // after all the widget control is moved over.
1892 public boolean isSubjectEmpty() {
1893 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1894 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001895
Mindy Pereira181df782012-03-01 13:32:44 -08001896 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001897 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07001898 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
1899 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001900 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001901
Mindy Pereirac2031972012-04-03 09:38:35 -07001902 String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
1903
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001904 MessageModification.putToAddresses(values, message.getToAddresses());
1905 MessageModification.putCcAddresses(values, message.getCcAddresses());
1906 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001907
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001908 MessageModification.putCustomFromAddress(values, message.from);
Mindy Pereira92551d02012-04-05 11:31:12 -07001909
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001910 MessageModification.putSubject(values, message.subject);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001911 String htmlBody = Html.toHtml(body);
Paul Westbrook05b92b82012-04-20 13:29:37 -07001912
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001913 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1914 StringBuilder fullBody = new StringBuilder(htmlBody);
1915 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001916 // HTML gets converted to text for now
1917 final String text = quotedText.toString();
1918 if (QuotedTextView.containsQuotedText(text)) {
1919 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07001920 final int quoteStartPos = fullBody.length() + pos;
1921 fullBody.append(text);
1922 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07001923 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07001924 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001925 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001926 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1927 // This shouldn't happen, but just use what we have,
1928 // and don't do server-side expansion
1929 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001930 }
1931 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001932 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07001933 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07001934 if (refMessage != null) {
1935 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1936 MessageModification.putBodyHtml(values, fullBody.toString());
1937 }
1938 if (!TextUtils.isEmpty(refMessage.bodyText)) {
1939 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1940 }
1941 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07001942 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07001943 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1944 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001945 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07001946 if (!TextUtils.isEmpty(refMessageId)) {
1947 MessageModification.putRefMessageId(values, refMessageId);
1948 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001949
Mindy Pereira92551d02012-04-05 11:31:12 -07001950 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(replyFromAccount,
Mindy Pereira181df782012-03-01 13:32:44 -08001951 values, refMessageId, save);
1952 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001953
Mindy Pereira181df782012-03-01 13:32:44 -08001954 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001955
Mindy Pereira181df782012-03-01 13:32:44 -08001956 // Do the send/save action on the specified handler to avoid possible
1957 // ANRs
1958 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001959
Mindy Pereira181df782012-03-01 13:32:44 -08001960 return sendOrSaveMessage.requestId();
1961 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001962
Mindy Pereira002ff522012-05-30 10:31:26 -07001963 private static int getDraftType(int mode) {
1964 int draftType = -1;
1965 switch (mode) {
1966 case ComposeActivity.COMPOSE:
1967 draftType = DraftType.COMPOSE;
1968 break;
1969 case ComposeActivity.REPLY:
1970 draftType = DraftType.REPLY;
1971 break;
1972 case ComposeActivity.REPLY_ALL:
1973 draftType = DraftType.REPLY_ALL;
1974 break;
1975 case ComposeActivity.FORWARD:
1976 draftType = DraftType.FORWARD;
1977 break;
1978 }
1979 return draftType;
1980 }
1981
Mindy Pereira181df782012-03-01 13:32:44 -08001982 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1983 boolean orientationChanged) {
1984 // Check if user is a monkey. Monkeys can compose and hit send
1985 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07001986 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08001987 return;
1988 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001989
Mindy Pereira181df782012-03-01 13:32:44 -08001990 String[] to, cc, bcc;
1991 if (orientationChanged) {
1992 to = cc = bcc = new String[0];
1993 } else {
1994 to = getToAddresses();
1995 cc = getCcAddresses();
1996 bcc = getBccAddresses();
1997 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001998
Mindy Pereira181df782012-03-01 13:32:44 -08001999 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002000 private int mRestoredRequestId;
2001
Marc Blank0bbc8582012-04-23 15:07:57 -07002002 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002003 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002004 synchronized (mActiveTasks) {
2005 int numTasks = mActiveTasks.size();
2006 if (numTasks == 0) {
2007 // Start service so we won't be killed if this app is
2008 // put in the background.
2009 startService(new Intent(ComposeActivity.this, EmptyService.class));
2010 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002011
Mindy Pereira181df782012-03-01 13:32:44 -08002012 mActiveTasks.add(sendOrSaveTask);
2013 }
2014 if (sTestSendOrSaveCallback != null) {
2015 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2016 }
2017 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002018
Marc Blank0bbc8582012-04-23 15:07:57 -07002019 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002020 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2021 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002022 synchronized (mDraftLock) {
2023 mDraftId = message.id;
2024 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002025 if (sRequestMessageIdMap != null) {
2026 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2027 }
Mindy Pereira181df782012-03-01 13:32:44 -08002028 // Cache request message map, in case the process is killed
2029 saveRequestMap();
2030 }
2031 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002032 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002033 }
2034 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002035
Marc Blank0bbc8582012-04-23 15:07:57 -07002036 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002037 public Message getMessage() {
2038 synchronized (mDraftLock) {
2039 return mDraft;
2040 }
2041 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002042
Marc Blank0bbc8582012-04-23 15:07:57 -07002043 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002044 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
2045 if (success) {
2046 // Successfully sent or saved so reset change markers
2047 discardChanges();
2048 } else {
2049 // A failure happened with saving/sending the draft
2050 // TODO(pwestbro): add a better string that should be used
2051 // when failing to send or save
2052 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2053 .show();
2054 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002055
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002056 int numTasks;
2057 synchronized (mActiveTasks) {
2058 // Remove the task from the list of active tasks
2059 mActiveTasks.remove(task);
2060 numTasks = mActiveTasks.size();
2061 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002062
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002063 if (numTasks == 0) {
2064 // Stop service so we can be killed.
2065 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2066 }
2067 if (sTestSendOrSaveCallback != null) {
2068 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2069 }
2070 }
Mindy Pereira181df782012-03-01 13:32:44 -08002071 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002072
Mindy Pereira181df782012-03-01 13:32:44 -08002073 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07002074 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002075 String fromAddress = selectedAccount.name;
2076 if (selectedAccount == null || fromAddress == null) {
2077 // We don't have either the selected account or from address,
2078 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07002079 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002080 fromAddress = mAccount.name;
2081 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002082
Mindy Pereira181df782012-03-01 13:32:44 -08002083 if (mSendSaveTaskHandler == null) {
2084 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2085 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002086
Mindy Pereira181df782012-03-01 13:32:44 -08002087 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2088 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002089
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002090 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002091 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2092 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Mindy Pereira12575862012-03-21 16:30:54 -07002093 mSendSaveTaskHandler, save, mComposeMode);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002094
Mindy Pereira181df782012-03-01 13:32:44 -08002095 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
2096 mRecipient = selectedAccount.name;
2097 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002098 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002099
Mindy Pereira181df782012-03-01 13:32:44 -08002100 // Don't display the toast if the user is just changing the orientation,
2101 // but we still need to save the draft to the cursor because this is how we restore
2102 // the attachments when the configuration change completes.
2103 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2104 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2105 Toast.LENGTH_LONG).show();
2106 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002107
Mindy Pereira181df782012-03-01 13:32:44 -08002108 // Need to update variables here because the send or save completes
2109 // asynchronously even though the toast shows right away.
2110 discardChanges();
2111 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002112
Mindy Pereira181df782012-03-01 13:32:44 -08002113 // If we are sending, finish the activity
2114 if (!save) {
2115 finish();
2116 }
2117 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002118
Mindy Pereira181df782012-03-01 13:32:44 -08002119 /**
2120 * Save the state of the request messageid map. This allows for the Gmail
2121 * process to be killed, but and still allow for ComposeActivity instances
2122 * to be recreated correctly.
2123 */
2124 private void saveRequestMap() {
2125 // TODO: store the request map in user preferences.
2126 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002127
Mindy Pereira013194c2012-01-06 15:09:33 -08002128 public void doAttach() {
2129 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2130 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002131 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Mindy Pereira181df782012-03-01 13:32:44 -08002132 if (android.provider.Settings.System.getInt(getContentResolver(),
2133 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002134 i.setType("*/*");
2135 } else {
2136 i.setType("image/*");
2137 }
2138 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002139 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2140 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002141 }
2142
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002143 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002144 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002145 if (mCcBccButton != null) {
2146 mCcBccButton.setVisibility(View.GONE);
2147 }
2148 }
2149
Mindy Pereira326c6602012-01-04 15:32:42 -08002150 @Override
2151 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002152 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002153 if (position == ComposeActivity.REPLY) {
2154 mComposeMode = ComposeActivity.REPLY;
2155 } else if (position == ComposeActivity.REPLY_ALL) {
2156 mComposeMode = ComposeActivity.REPLY_ALL;
2157 } else if (position == ComposeActivity.FORWARD) {
2158 mComposeMode = ComposeActivity.FORWARD;
2159 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002160 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002161 resetMessageForModeChange();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002162 if (mRefMessage != null) {
2163 initFromRefMessage(mComposeMode, mAccount.name);
2164 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002165 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002166 return true;
2167 }
2168
Mindy Pereira154386a2012-01-11 13:02:33 -08002169 private void resetMessageForModeChange() {
2170 // When switching between reply, reply all, forward,
2171 // follow the behavior of webview.
2172 // The contents of the following fields are cleared
2173 // so that they can be populated directly from the
2174 // ref message:
2175 // 1) Any recipient fields
2176 // 2) The subject
2177 mTo.setText("");
2178 mCc.setText("");
2179 mBcc.setText("");
2180 // Any edits to the subject are replaced with the original subject.
2181 mSubject.setText("");
2182
2183 // Any changes to the contents of the following fields are kept:
2184 // 1) Body
2185 // 2) Attachments
2186 // If the user made changes to attachments, keep their changes.
2187 if (!mAttachmentsChanged) {
2188 mAttachmentsView.deleteAllAttachments();
2189 }
2190 }
2191
Mindy Pereira326c6602012-01-04 15:32:42 -08002192 private class ComposeModeAdapter extends ArrayAdapter<String> {
2193
2194 private LayoutInflater mInflater;
2195
2196 public ComposeModeAdapter(Context context) {
2197 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2198 .getStringArray(R.array.compose_modes));
2199 }
2200
2201 private LayoutInflater getInflater() {
2202 if (mInflater == null) {
2203 mInflater = LayoutInflater.from(getContext());
2204 }
2205 return mInflater;
2206 }
2207
2208 @Override
2209 public View getView(int position, View convertView, ViewGroup parent) {
2210 if (convertView == null) {
2211 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2212 }
2213 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2214 return super.getView(position, convertView, parent);
2215 }
2216 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002217
2218 @Override
2219 public void onRespondInline(String text) {
2220 appendToBody(text, false);
2221 }
2222
2223 /**
2224 * Append text to the body of the message. If there is no existing body
2225 * text, just sets the body to text.
2226 *
2227 * @param text
2228 * @param withSignature True to append a signature.
2229 */
2230 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002231 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002232 if (bodyText != null && bodyText.length() > 0) {
2233 bodyText.append(text);
2234 } else {
2235 setBody(text, withSignature);
2236 }
2237 }
2238
2239 /**
2240 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002241 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002242 * @param text
2243 * @param withSignature True to append a signature.
2244 */
2245 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002246 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002247 if (withSignature) {
2248 appendSignature();
2249 }
2250 }
2251
2252 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002253 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002254 boolean hasFocus = mBodyView.hasFocus();
Mindy Pereirab13917c2012-03-29 08:08:19 -07002255 if (!TextUtils.equals(newSignature, mSignature)) {
2256 mSignature = newSignature;
2257 if (!TextUtils.isEmpty(mSignature)
2258 && getSignatureStartPosition(mSignature,
2259 mBodyView.getText().toString()) < 0) {
2260 // Appending a signature does not count as changing text.
2261 mBodyView.removeTextChangedListener(this);
2262 mBodyView.append(convertToPrintableSignature(mSignature));
2263 mBodyView.addTextChangedListener(this);
2264 }
Mindy Pereira433b1982012-04-03 11:53:07 -07002265 if (hasFocus) {
2266 focusBody();
2267 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002268 }
2269 }
2270
2271 private String convertToPrintableSignature(String signature) {
2272 String signatureResource = getResources().getString(R.string.signature);
2273 if (signature == null) {
2274 signature = "";
2275 }
2276 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002277 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002278
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08002279 @Override
2280 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07002281 mReplyFromAccount = mFromSpinner.getCurrentAccount();
2282 if (!mAccount.equals(mReplyFromAccount.account)) {
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002283 setAccount(mReplyFromAccount.account);
2284
Mindy Pereira181df782012-03-01 13:32:44 -08002285 // TODO: handle discarding attachments when switching accounts.
2286 // Only enable save for this draft if there is any other content
2287 // in the message.
2288 if (!isBlank()) {
2289 enableSave(true);
2290 }
2291 mReplyFromChanged = true;
2292 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002293 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002294 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002295
2296 public void enableSave(boolean enabled) {
2297 if (mSave != null) {
2298 mSave.setEnabled(enabled);
2299 }
2300 }
2301
2302 public void enableSend(boolean enabled) {
2303 if (mSend != null) {
2304 mSend.setEnabled(enabled);
2305 }
2306 }
2307
2308 /**
2309 * Handles button clicks from any error dialogs dealing with sending
2310 * a message.
2311 */
2312 @Override
2313 public void onClick(DialogInterface dialog, int which) {
2314 switch (which) {
2315 case DialogInterface.BUTTON_POSITIVE: {
2316 doDiscardWithoutConfirmation(true /* show toast */ );
2317 break;
2318 }
2319 case DialogInterface.BUTTON_NEGATIVE: {
2320 // If the user cancels the send, re-enable the send button.
2321 enableSend(true);
2322 break;
2323 }
2324 }
2325
2326 }
2327
Mindy Pereiraefe3d252012-03-01 14:20:44 -08002328 private void doDiscard() {
2329 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2330 .setPositiveButton(R.string.ok, this)
2331 .setNegativeButton(R.string.cancel, null)
2332 .create().show();
2333 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002334 /**
2335 * Effectively discard the current message.
2336 *
2337 * This method is either invoked from the menu or from the dialog
2338 * once the user has confirmed that they want to discard the message.
2339 * @param showToast show "Message discarded" toast if true
2340 */
2341 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002342 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002343 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2344 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07002345 values.put(BaseColumns._ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002346 if (mAccount.expungeMessageUri != null) {
2347 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2348 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07002349 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002350 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002351 // This is not strictly necessary (since we should not try to
2352 // save the draft after calling this) but it ensures that if we
2353 // do save again for some reason we make a new draft rather than
2354 // trying to resave an expunged draft.
2355 mDraftId = UIProvider.INVALID_MESSAGE_ID;
2356 }
2357 }
2358
2359 if (showToast) {
2360 // Display a toast to let the user know
2361 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2362 }
2363
2364 // This prevents the draft from being saved in onPause().
2365 discardChanges();
2366 finish();
2367 }
2368
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002369 private void saveIfNeeded() {
2370 if (mAccount == null) {
2371 // We have not chosen an account yet so there's no way that we can save. This is ok,
2372 // though, since we are saving our state before AccountsActivity is activated. Thus, the
2373 // user has not interacted with us yet and there is no real state to save.
2374 return;
2375 }
2376
2377 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07002378 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002379 }
2380 }
2381
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002382 @Override
2383 public void onAttachmentDeleted() {
2384 mAttachmentsChanged = true;
2385 updateSaveUi();
2386 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002387
2388
2389 /**
2390 * This is called any time one of our text fields changes.
2391 */
Marc Blank0bbc8582012-04-23 15:07:57 -07002392 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002393 public void afterTextChanged(Editable s) {
2394 mTextChanged = true;
2395 updateSaveUi();
2396 }
2397
2398 @Override
2399 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2400 // Do nothing.
2401 }
2402
Marc Blank0bbc8582012-04-23 15:07:57 -07002403 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002404 public void onTextChanged(CharSequence s, int start, int before, int count) {
2405 // Do nothing.
2406 }
2407
2408
2409 // There is a big difference between the text associated with an address changing
2410 // to add the display name or to format properly and a recipient being added or deleted.
2411 // Make sure we only notify of changes when a recipient has been added or deleted.
2412 private class RecipientTextWatcher implements TextWatcher {
2413 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2414
2415 private RecipientEditTextView mView;
2416
2417 private TextWatcher mListener;
2418
2419 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2420 mView = view;
2421 mListener = listener;
2422 }
2423
2424 @Override
2425 public void afterTextChanged(Editable s) {
2426 if (hasChanged()) {
2427 mListener.afterTextChanged(s);
2428 }
2429 }
2430
2431 private boolean hasChanged() {
2432 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2433 int totalCount = currRecips.length;
2434 int totalPrevCount = 0;
2435 for (Entry<String, Integer> entry : mContent.entrySet()) {
2436 totalPrevCount += entry.getValue();
2437 }
2438 if (totalCount != totalPrevCount) {
2439 return true;
2440 }
2441
2442 for (String recip : currRecips) {
2443 if (!mContent.containsKey(recip)) {
2444 return true;
2445 } else {
2446 int count = mContent.get(recip) - 1;
2447 if (count < 0) {
2448 return true;
2449 } else {
2450 mContent.put(recip, count);
2451 }
2452 }
2453 }
2454 return false;
2455 }
2456
2457 private String[] tokenizeRecips(String[] recips) {
2458 // Tokenize them all and put them in the list.
2459 String[] recipAddresses = new String[recips.length];
2460 for (int i = 0; i < recips.length; i++) {
2461 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2462 }
2463 return recipAddresses;
2464 }
2465
2466 @Override
2467 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2468 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2469 for (String recip : recips) {
2470 if (!mContent.containsKey(recip)) {
2471 mContent.put(recip, 1);
2472 } else {
2473 mContent.put(recip, (mContent.get(recip)) + 1);
2474 }
2475 }
2476 }
2477
2478 @Override
2479 public void onTextChanged(CharSequence s, int start, int before, int count) {
2480 // Do nothing.
2481 }
2482 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002483
2484 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
2485 if (sTestSendOrSaveCallback != null && testCallback != null) {
2486 throw new IllegalStateException("Attempting to register more than one test callback");
2487 }
2488 sTestSendOrSaveCallback = testCallback;
2489 }
Vikram Aggarwal8183d452012-04-17 09:13:29 -07002490}