blob: eb3f42a88c652406c19ad75d0dcec23a45e4eeed [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;
Mindy Pereira82cc5662012-01-09 17:29:30 -080020import android.app.ActivityManager;
21import android.app.AlertDialog;
22import android.app.Dialog;
Mindy Pereira326c6602012-01-04 15:32:42 -080023import android.app.ActionBar.OnNavigationListener;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080024import android.app.Activity;
Mindy Pereira181df782012-03-01 13:32:44 -080025import android.app.LoaderManager.LoaderCallbacks;
Mindy Pereira6349a042012-01-04 11:25:01 -080026import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080027import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080028import android.content.Context;
Mindy Pereira181df782012-03-01 13:32:44 -080029import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080030import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080031import android.content.Intent;
Mindy Pereira181df782012-03-01 13:32:44 -080032import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080033import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080034import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080035import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080036import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080037import android.os.Handler;
38import android.os.HandlerThread;
Paul Westbrookf97588b2012-03-20 11:11:37 -070039import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080040import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080041import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080042import android.text.Html;
43import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080044import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080045import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080046import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080047import android.text.util.Rfc822Tokenizer;
Mindy Pereira8eca57a2012-03-20 16:42:34 -070048import android.view.Gravity;
Mindy Pereira326c6602012-01-04 15:32:42 -080049import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080050import android.view.Menu;
51import android.view.MenuInflater;
52import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080053import android.view.View;
Mindy Pereira326c6602012-01-04 15:32:42 -080054import android.view.ViewGroup;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080055import android.view.View.OnClickListener;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080056import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080057import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080058import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070059import android.widget.EditText;
Mindy Pereira1f936682012-03-02 11:30:33 -080060import android.widget.ImageView;
Mindy Pereira6349a042012-01-04 11:25:01 -080061import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080062import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080063
Mindy Pereirac17d0732011-12-29 10:46:19 -080064import com.android.common.Rfc822Validator;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080065import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080066import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080067import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080068import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080069import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080070import com.android.mail.providers.Address;
71import com.android.mail.providers.Attachment;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080072import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080073import com.android.mail.providers.MessageModification;
Mindy Pereira181df782012-03-01 13:32:44 -080074import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080075import com.android.mail.providers.UIProvider;
Mindy Pereira12575862012-03-21 16:30:54 -070076import com.android.mail.providers.UIProvider.DraftType;
Andy Huang30e2c242012-01-06 18:14:30 -080077import com.android.mail.R;
Paul Westbrook92227f62012-03-20 10:32:51 -070078import com.android.mail.utils.AccountUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080079import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080080import com.android.mail.utils.Utils;
Mindy Pereirac17d0732011-12-29 10:46:19 -080081import com.android.ex.chips.RecipientEditTextView;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080082import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080083import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080084import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080085
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,
Mindy Pereira181df782012-03-01 13:32:44 -0800100 AttachmentDeletedListener, OnAccountChangedListener, LoaderCallbacks<Cursor> {
Mindy Pereira6349a042012-01-04 11:25:01 -0800101 // Identifiers for which type of composition this is
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700102 static final int COMPOSE = -1;
Mindy Pereira6349a042012-01-04 11:25:01 -0800103 static final int REPLY = 0;
104 static final int REPLY_ALL = 1;
105 static final int FORWARD = 2;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700106 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800107
108 // Integer extra holding one of the above compose action
109 private static final String EXTRA_ACTION = "action";
110
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700111 private static final String UTF8_ENCODING_NAME = "UTF-8";
112
113 private static final String MAIL_TO = "mailto";
114
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700115 private static final String EXTRA_SUBJECT = "subject";
116
117 private static final String EXTRA_BODY = "body";
118
119 // Extra that we can get passed from other activities
120 private static final String EXTRA_TO = "to";
121 private static final String EXTRA_CC = "cc";
122 private static final String EXTRA_BCC = "bcc";
123
124 // List of all the fields
125 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
126
Mindy Pereira82cc5662012-01-09 17:29:30 -0800127 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
128 // Map containing information about requests to create new messages, and the id of the
129 // messages that were the result of those requests.
130 //
131 // This map is used when the activity that initiated the save a of a new message, is killed
132 // before the save has completed (and when we know the id of the newly created message). When
133 // a save is completed, the service that is running in the background, will update the map
134 //
135 // When a new ComposeActivity instance is created, it will attempt to use the information in
136 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
137 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
138 // to populate the map with data stored in shared preferences.
139 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
140 // Key used to store the above map
141 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800142 /**
143 * Notifies the {@code Activity} that the caller is an Email
144 * {@code Activity}, so that the back behavior may be modified accordingly.
145 *
146 * @see #onAppUpPressed
147 */
148 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
149
Paul Westbrookf97588b2012-03-20 11:11:37 -0700150 static final String EXTRA_ATTACHMENTS = "attachments";
151
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800152 // If this is a reply/forward then this extra will hold the original message
153 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700154 // If this is an action to edit an existing draft messagge, this extra will hold the
155 // draft message
156 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800157 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800158 private static final String LOG_TAG = new LogUtils().getLogTag();
159 // Request numbers for activities we start
160 private static final int RESULT_PICK_ATTACHMENT = 1;
161 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira181df782012-03-01 13:32:44 -0800162 private static final int ACCOUNT_SETTINGS_LOADER = 0;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700163 // TODO(mindyp) set mime-type for auto send?
164 private static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
165
166 // Max size for attachments (5 megs). Will be overridden by account settings if found.
167 // TODO(mindyp): read this from account settings?
168 private static final int DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800169
Mindy Pereira82cc5662012-01-09 17:29:30 -0800170 /**
171 * A single thread for running tasks in the background.
172 */
173 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800174 private RecipientEditTextView mTo;
175 private RecipientEditTextView mCc;
176 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800177 private Button mCcBccButton;
178 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800179 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800180 private Account mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800181 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800182 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800183 private TextView mSubject;
184
Mindy Pereira326c6602012-01-04 15:32:42 -0800185 private ComposeModeAdapter mComposeModeAdapter;
186 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800187 private boolean mForward;
188 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800189 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700190 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800191 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800192 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800193 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800194 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800195 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800196 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800197 private boolean mTextChanged;
198 private boolean mReplyFromChanged;
199 private MenuItem mSave;
200 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800201 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800202 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800203 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800204 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
205 private Message mDraft;
206 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800207 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800208
Mindy Pereira326c6602012-01-04 15:32:42 -0800209 /**
210 * Can be called from a non-UI thread.
211 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800212 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700213 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800214 }
215
Mindy Pereira6349a042012-01-04 11:25:01 -0800216 /**
217 * Can be called from a non-UI thread.
218 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800219 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800220 launch(launcher, account, null, COMPOSE);
221 }
222
223 /**
224 * Can be called from a non-UI thread.
225 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800226 public static void reply(Context launcher, Account account, Message message) {
227 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800228 }
229
230 /**
231 * Can be called from a non-UI thread.
232 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800233 public static void replyAll(Context launcher, Account account, Message message) {
234 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800235 }
236
237 /**
238 * Can be called from a non-UI thread.
239 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800240 public static void forward(Context launcher, Account account, Message message) {
241 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800242 }
243
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800244 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800245 Intent intent = new Intent(launcher, ComposeActivity.class);
246 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
247 intent.putExtra(EXTRA_ACTION, action);
248 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700249 if (action == EDIT_DRAFT) {
250 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
251 } else {
252 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
253 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800254 launcher.startActivity(intent);
255 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800256
257 @Override
258 public void onCreate(Bundle savedInstanceState) {
259 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800260 setContentView(R.layout.compose);
261 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800262 Intent intent = getIntent();
Paul Westbrook92227f62012-03-20 10:32:51 -0700263
264 Account account = (Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
265 if (account == null) {
266 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
267 if (syncingAccounts.length > 0) {
268 account = syncingAccounts[0];
269 }
270 }
271
272 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800273 if (mAccount == null) {
274 return;
275 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800276 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800277 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereira29ef1b82012-01-13 11:26:21 -0800278 if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700279 if (mRefMessage != null) {
280 initFromRefMessage(action, mAccount.name);
281 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700282 } else if (action == EDIT_DRAFT) {
283 // Initialize the message from the message in the intent
284 final Message message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
285
286 initFromMessage(message);
287
288 // Update the action to the draft type of the previous draft
289 switch (message.draftType) {
290 case UIProvider.DraftType.REPLY:
291 action = REPLY;
292 break;
293 case UIProvider.DraftType.REPLY_ALL:
294 action = REPLY_ALL;
295 break;
296 case UIProvider.DraftType.FORWARD:
297 action = FORWARD;
298 break;
299 case UIProvider.DraftType.COMPOSE:
300 default:
301 action = COMPOSE;
302 break;
303 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700304 } else {
305 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700306 }
307
308 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800309 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800310 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800311 initRecipients();
Paul Westbrookf97588b2012-03-20 11:11:37 -0700312 initAttachmentsFromIntent(intent);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800313 initActionBar(action);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700314 initFromSpinner(action);
Mindy Pereira75f66632012-01-11 11:42:02 -0800315 initChangeListeners();
Mindy Pereira433b1982012-04-03 11:53:07 -0700316 setFocus(action);
317 }
318
319 private void setFocus(int action) {
320 if (action == EDIT_DRAFT) {
321 int type = mDraft.draftType;
322 switch (type) {
323 case UIProvider.DraftType.COMPOSE:
324 case UIProvider.DraftType.FORWARD:
325 action = COMPOSE;
326 break;
327 case UIProvider.DraftType.REPLY:
328 case UIProvider.DraftType.REPLY_ALL:
329 default:
330 action = REPLY;
331 break;
332 }
333 }
334 switch (action) {
335 case FORWARD:
336 case COMPOSE:
337 mTo.requestFocus();
338 break;
339 case REPLY:
340 case REPLY_ALL:
341 default:
342 focusBody();
343 break;
344 }
345 }
346
347 /**
348 * Focus the body of the message.
349 */
350 public void focusBody() {
351 mBodyView.requestFocus();
352 int length = mBodyView.getText().length();
353
354 int signatureStartPos = getSignatureStartPosition(
355 mSignature, mBodyView.getText().toString());
356 if (signatureStartPos > -1) {
357 // In case the user deleted the newlines...
358 mBodyView.setSelection(signatureStartPos);
359 } else if (length > 0) {
360 // Move cursor to the end.
361 mBodyView.setSelection(length);
362 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800363 }
364
365 @Override
366 protected void onResume() {
367 super.onResume();
368 // Update the from spinner as other accounts
369 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800370 if (mFromSpinner != null && mAccount != null) {
371 mFromSpinner.asyncInitFromSpinner();
372 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800373 }
374
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800375 @Override
376 protected void onPause() {
377 super.onPause();
378
379 if (mSendConfirmDialog != null) {
380 mSendConfirmDialog.dismiss();
381 }
382 if (mRecipientErrorDialog != null) {
383 mRecipientErrorDialog.dismiss();
384 }
385
386 saveIfNeeded();
387 }
388
389 @Override
390 protected final void onActivityResult(int request, int result, Intent data) {
391 mAddingAttachment = false;
392
393 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
394 addAttachmentAndUpdateView(data);
395 }
396 }
397
398 @Override
399 public final void onSaveInstanceState(Bundle state) {
400 super.onSaveInstanceState(state);
401
402 // onSaveInstanceState is only called if the user might come back to this activity so it is
403 // not an ideal location to save the draft. However, if we have never saved the draft before
404 // we have to save it here in order to have an id to save in the bundle.
405 saveIfNeededOnOrientationChanged();
406 }
407
Mindy Pereira818143e2012-01-11 13:59:49 -0800408 @VisibleForTesting
409 void setAccount(Account account) {
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700410 assert(account != null);
411 if (!account.equals(mAccount)) {
412 mAccount = account;
413 }
Mindy Pereira181df782012-03-01 13:32:44 -0800414 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800415 }
416
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700417 private void initFromSpinner(int action) {
418 if (action == COMPOSE ||
419 (action == EDIT_DRAFT
420 && mDraft.draftType == UIProvider.DraftType.COMPOSE)) {
421 mFromSpinner.setCurrentAccount(mAccount);
422 mFromSpinner.asyncInitFromSpinner();
423 boolean showSpinner = mFromSpinner.getCount() > 1;
424 // If there is only 1 account, just show that account.
425 // Otherwise, give the user the ability to choose which account to
426 // send
427 // mail from / save drafts to.
428 mFromStatic.setVisibility(showSpinner ? View.GONE : View.VISIBLE);
429 mFromStaticText.setText(mAccount.name);
430 mFromSpinnerWrapper.setVisibility(showSpinner ? View.VISIBLE : View.GONE);
431 } else {
432 mFromStatic.setVisibility(View.VISIBLE);
433 mFromStaticText.setText(mAccount.name);
434 mFromSpinnerWrapper.setVisibility(View.GONE);
435 mFromSpinner.setCurrentAccount(mAccount);
436 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800437 }
438
439 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800440 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800441 if (mCcBccButton != null) {
442 mCcBccButton.setOnClickListener(this);
443 }
444 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800445 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800446 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
447 if (mAttachmentsButton != null) {
448 mAttachmentsButton.setOnClickListener(this);
449 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800450 mTo = (RecipientEditTextView) findViewById(R.id.to);
451 mCc = (RecipientEditTextView) findViewById(R.id.cc);
452 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800453 // TODO: add special chips text change watchers before adding
454 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800455 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800456 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
457 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -0700458 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800459 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800460 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800461 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800462 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800463 }
464
Mindy Pereira75f66632012-01-11 11:42:02 -0800465 // Now that the message has been initialized from any existing draft or
466 // ref message data, set up listeners for any changes that occur to the
467 // message.
468 private void initChangeListeners() {
469 mSubject.addTextChangedListener(this);
470 mBodyView.addTextChangedListener(this);
471 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
472 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
473 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
474 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800475 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800476 }
477
Mindy Pereira326c6602012-01-04 15:32:42 -0800478 private void initActionBar(int action) {
479 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800480 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800481 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800482 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
483 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800484 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800485 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800486 if (mComposeModeAdapter == null) {
487 mComposeModeAdapter = new ComposeModeAdapter(this);
488 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800489 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
490 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800491 switch (action) {
492 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800493 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800494 break;
495 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800496 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800497 break;
498 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800499 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800500 break;
501 }
502 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700503 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
504 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
505 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800506 }
507
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800508 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700509 setSubject(mRefMessage, action);
510 // Setup recipients
511 if (action == FORWARD) {
512 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800513 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700514 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
515 initBodyFromRefMessage(mRefMessage, action);
516 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
517 initAttachments(mRefMessage);
518 }
519 updateHideOrShowCcBcc();
Mindy Pereirac17d0732011-12-29 10:46:19 -0800520 }
521
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700522 private void initFromMessage(Message message) {
523 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
524
525 mDraft = message;
526 mDraftId = message.id;
527 mSubject.setText(message.subject);
528 mForward = message.draftType == UIProvider.DraftType.FORWARD;
529 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
530 addToAddresses(toAddresses);
531 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
532 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -0700533 if (message.hasAttachments) {
534 List<Attachment> attachments = message.getAttachments();
535 for (Attachment a : attachments) {
536 addAttachmentAndUpdateView(a.uri);
537 }
538 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700539
540 // Set the body
541 if (!TextUtils.isEmpty(message.bodyHtml)) {
542 mBodyView.setText(Html.fromHtml(message.bodyHtml));
543 } else {
544 mBodyView.setText(message.bodyText);
545 }
546
547 // TODO: load attachments from the previous message
548 // TODO: set the from address spinner to the right from account
549 // TODO: initialize quoted text value
550 }
551
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700552 /**
553 * Fill all the widgets with the content found in the Intent Extra, if any.
554 * Also apply the same style to all widgets. Note: if initFromExtras is
555 * called as a result of switching between reply, reply all, and forward per
556 * the latest revision of Gmail, and the user has already made changes to
557 * attachments on a previous incarnation of the message (as a reply, reply
558 * all, or forward), the original attachments from the message will not be
559 * re-instantiated. The user's changes will be respected. This follows the
560 * web gmail interaction.
561 */
562 public void initFromExtras(Intent intent) {
563
564 // If we were invoked with a SENDTO intent, the value
565 // should take precedence
566 final Uri dataUri = intent.getData();
567 if (dataUri != null) {
568 if (MAIL_TO.equals(dataUri.getScheme())) {
569 initFromMailTo(dataUri.toString());
570 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -0700571 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700572 String toText = dataUri.getSchemeSpecificPart();
573 if (toText != null) {
574 mTo.setText("");
575 addToAddresses(Arrays.asList(toText.split(",")));
576 }
577 }
578 }
579 }
580
581 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
582 if (extraStrings != null) {
583 addToAddresses(Arrays.asList(extraStrings));
584 }
585 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
586 if (extraStrings != null) {
587 addCcAddresses(Arrays.asList(extraStrings), null);
588 }
589 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
590 if (extraStrings != null) {
591 addBccAddresses(Arrays.asList(extraStrings));
592 }
593
594 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
595 if (extraString != null) {
596 mSubject.setText(extraString);
597 }
598
599 for (String extra : ALL_EXTRAS) {
600 if (intent.hasExtra(extra)) {
601 String value = intent.getStringExtra(extra);
602 if (EXTRA_TO.equals(extra)) {
603 addToAddresses(Arrays.asList(value.split(",")));
604 } else if (EXTRA_CC.equals(extra)) {
605 addCcAddresses(Arrays.asList(value.split(",")), null);
606 } else if (EXTRA_BCC.equals(extra)) {
607 addBccAddresses(Arrays.asList(value.split(",")));
608 } else if (EXTRA_SUBJECT.equals(extra)) {
609 mSubject.setText(value);
610 } else if (EXTRA_BODY.equals(extra)) {
611 setBody(value, true /* with signature */);
612 }
613 }
614 }
615
616 Bundle extras = intent.getExtras();
617 if (extras != null) {
618 final String action = intent.getAction();
619 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
620 if (text != null) {
621 setBody(text, true /* with signature */);
622 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700623 }
624
625 updateHideOrShowCcBcc();
626 }
627
628 @VisibleForTesting
629 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
630 // TODO: handle the case where there are spaces in the display name as well as the email
631 // such as "Guy with spaces <guy+with+spaces@gmail.com>" as they it could be encoded
632 // ambiguously.
633
634 // Since URLDecode.decode changes + into ' ', and + is a valid
635 // email character, we need to find/ replace these ourselves before
636 // decoding.
637 String replacePlus = s.replace("+", "%2B");
638 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
639 }
640
641 /**
642 * Initialize the compose view from a String representing a mailTo uri.
643 * @param mailToString The uri as a string.
644 */
645 public void initFromMailTo(String mailToString) {
646 // We need to disguise this string as a URI in order to parse it
647 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
648 Uri uri = Uri.parse("foo://" + mailToString);
649 int index = mailToString.indexOf("?");
650 int length = "mailto".length() + 1;
651 String to;
652 try {
653 // Extract the recipient after mailto:
654 if (index == -1) {
655 to = decodeEmailInUri(mailToString.substring(length));
656 } else {
657 to = decodeEmailInUri(mailToString.substring(length, index));
658 }
659 addToAddresses(Arrays.asList(to.split(" ,")));
660 } catch (UnsupportedEncodingException e) {
661 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
662 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
663 } else {
664 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
665 }
666 }
667
668 List<String> cc = uri.getQueryParameters("cc");
669 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
670
671 List<String> otherTo = uri.getQueryParameters("to");
672 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
673
674 List<String> bcc = uri.getQueryParameters("bcc");
675 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
676
677 List<String> subject = uri.getQueryParameters("subject");
678 if (subject.size() > 0) {
679 try {
680 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
681 } catch (UnsupportedEncodingException e) {
682 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
683 e.getMessage(), subject);
684 }
685 }
686
687 List<String> body = uri.getQueryParameters("body");
688 if (body.size() > 0) {
689 try {
690 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
691 true /* with signature */);
692 } catch (UnsupportedEncodingException e) {
693 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
694 }
695 }
696
697 updateHideOrShowCcBcc();
698 }
699
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800700 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800701 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800702 }
703
Paul Westbrookf97588b2012-03-20 11:11:37 -0700704 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -0700705 Bundle extras = intent.getExtras();
706 if (extras == null) {
707 extras = Bundle.EMPTY;
708 }
Paul Westbrookf97588b2012-03-20 11:11:37 -0700709 final String action = intent.getAction();
710 if (!mAttachmentsChanged) {
711 long totalSize = 0;
712 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
713 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
714 for (String uriString : uris) {
715 final Uri uri = Uri.parse(uriString);
716 long size = 0;
717 try {
718 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
719 true /* local file */);
720 } catch (AttachmentFailureException e) {
721 // A toast has already been shown to the user,
722 // just break out of the loop.
723 LogUtils.e(LOG_TAG, e, "Error adding attachment");
724 }
725 totalSize += size;
726 }
727 }
728 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
729 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
730 long size = 0;
731 try {
732 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
733 true /* local file */);
734 } catch (AttachmentFailureException e) {
735 // A toast has already been shown to the user, so just
736 // exit.
737 LogUtils.e(LOG_TAG, e, "Error adding attachment");
738 }
739 totalSize += size;
740 }
741
742 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
743 && extras.containsKey(Intent.EXTRA_STREAM)) {
744 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
745 for (Parcelable uri : uris) {
746 long size = 0;
747 try {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700748 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri,
Paul Westbrookf97588b2012-03-20 11:11:37 -0700749 false /* doSave */, true /* local file */);
750 } catch (AttachmentFailureException e) {
751 // A toast has already been shown to the user,
752 // just break out of the loop.
753 LogUtils.e(LOG_TAG, e, "Error adding attachment");
754 }
755 totalSize += size;
756 }
757 }
758
759 if (totalSize > 0) {
760 mAttachmentsChanged = true;
761 updateSaveUi();
762 }
763 }
764 }
765
766
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800767 private void initBodyFromRefMessage(Message refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800768 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800769 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
770 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800771 }
772
773 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800774 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800775 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
776 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
777 if (ccVisible || bccVisible) {
778 mCcBccView.show(false, ccVisible, bccVisible);
779 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800780 if (mCcBccButton != null) {
781 if (!mCc.isShown() || !mBcc.isShown()) {
782 mCcBccButton.setVisibility(View.VISIBLE);
783 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
784 : R.string.add_bcc_label));
785 } else {
786 mCcBccButton.setVisibility(View.GONE);
787 }
788 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800789 }
790
Mindy Pereira013194c2012-01-06 15:09:33 -0800791 /**
792 * Add attachment and update the compose area appropriately.
793 * @param data
794 */
795 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -0700796 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
797 }
798
799 public void addAttachmentAndUpdateView(Uri uri) {
800 if (uri == null) {
801 return;
802 }
Mindy Pereira013194c2012-01-06 15:09:33 -0800803 try {
Mindy Pereira2421dc82012-03-27 13:32:31 -0700804 long size = mAttachmentsView.addAttachment(mAccount, uri,
805 false /* doSave */,
Mindy Pereiraf944e962012-01-17 11:43:36 -0800806 true /* local file */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800807 if (size > 0) {
808 mAttachmentsChanged = true;
809 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800810 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800811 } catch (AttachmentFailureException e) {
812 // A toast has already been shown to the user, no need to do
813 // anything.
814 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800815 }
816 }
817
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800818 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800819 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800820 // Don't populate the address if this is a forward.
821 if (action == ComposeActivity.FORWARD) {
822 return;
823 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800824 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800825 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800826
Mindy Pereira818143e2012-01-11 13:59:49 -0800827 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800828 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800829 // This is the email address of the current user, i.e. the one composing
830 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800831 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800832 String fromAddress = refMessage.from;
833 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
834 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800835 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800836
837 // If this is a reply, the Cc list is empty. If this is a reply-all, the
838 // Cc list is the union of the To and Cc recipients of the original
839 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800840 // already on the To list.
841 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800842 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
843 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800844 addToAddresses(toAddresses);
845 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800846 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800847 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
848 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800849 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800850 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800851 addRecipients(accountEmail, ccAddresses,
852 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800853 addCcAddresses(ccAddresses, toAddresses);
854 }
855 }
856
857 private void addToAddresses(Collection<String> addresses) {
858 addAddressesToList(addresses, mTo);
859 }
860
861 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700862 addCcAddressesToList(tokenizeAddressList(addresses),
863 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800864 }
865
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700866 private void addBccAddresses(Collection<String> addresses) {
867 addAddressesToList(addresses, mBcc);
868 }
869
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800870 @VisibleForTesting
871 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
872 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
873 String address;
874
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700875 if (compareToList == null) {
876 for (Rfc822Token[] tokens : addresses) {
877 for (int i = 0; i < tokens.length; i++) {
878 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800879 list.append(address + END_TOKEN);
880 }
881 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700882 } else {
883 HashSet<String> compareTo = convertToHashSet(compareToList);
884 for (Rfc822Token[] tokens : addresses) {
885 for (int i = 0; i < tokens.length; i++) {
886 address = tokens[i].toString();
887 // Check if this is a duplicate:
888 if (!compareTo.contains(tokens[i].getAddress())) {
889 // Get the address here
890 list.append(address + END_TOKEN);
891 }
892 }
893 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800894 }
895 }
896
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800897 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
898 HashSet<String> hash = new HashSet<String>();
899 for (Rfc822Token[] tokens : list) {
900 for (int i = 0; i < tokens.length; i++) {
901 hash.add(tokens[i].getAddress());
902 }
903 }
904 return hash;
905 }
906
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800907 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
908 @VisibleForTesting
909 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
910
911 for (String address: addresses) {
912 tokenized.add(Rfc822Tokenizer.tokenize(address));
913 }
914 return tokenized;
915 }
916
917 @VisibleForTesting
918 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
919 for (String address : addresses) {
920 addAddressToList(address, list);
921 }
922 }
923
924 private void addAddressToList(String address, RecipientEditTextView list) {
925 if (address == null || list == null)
926 return;
927
928 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
929
930 for (int i = 0; i < tokens.length; i++) {
931 list.append(tokens[i] + END_TOKEN);
932 }
933 }
934
935 @VisibleForTesting
936 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800937 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800938 // The To recipient is the reply-to address specified in the original
939 // message, unless it is:
940 // the current user OR a custom from of the current user, in which case
941 // it's the To recipient list of the original message.
942 // OR missing, in which case use the sender of the original message
943 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800944 if (!TextUtils.isEmpty(replyToAddress)) {
945 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800946 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800947 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800948 }
949 return toAddresses;
950 }
951
952 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
953 for (String email : addresses) {
954 // Do not add this account, or any of the custom froms, to the list
955 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800956 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800957 if (!account.equalsIgnoreCase(recipientAddress)) {
958 recipients.add(email.replace("\"\"", ""));
959 }
960 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800961 }
962
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800963 private void setSubject(Message refMessage, int action) {
964 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800965 String prefix;
966 String correctedSubject = null;
967 if (action == ComposeActivity.COMPOSE) {
968 prefix = "";
969 } else if (action == ComposeActivity.FORWARD) {
970 prefix = getString(R.string.forward_subject_label);
971 } else {
972 prefix = getString(R.string.reply_subject_label);
973 }
974
975 // Don't duplicate the prefix
976 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
977 correctedSubject = subject;
978 } else {
979 correctedSubject = String
980 .format(getString(R.string.formatted_subject), prefix, subject);
981 }
982 mSubject.setText(correctedSubject);
983 }
984
Mindy Pereira818143e2012-01-11 13:59:49 -0800985 private void initRecipients() {
986 setupRecipients(mTo);
987 setupRecipients(mCc);
988 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800989 }
990
Mindy Pereira818143e2012-01-11 13:59:49 -0800991 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800992 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800993 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800994 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800995 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800996 int offset = accountName.indexOf("@") + 1;
997 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800998 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800999 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001000 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001001 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001002 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001003 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001004 }
1005
1006 @Override
1007 public void onClick(View v) {
1008 int id = v.getId();
1009 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001010 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001011 // Verify that cc/ bcc aren't showing.
1012 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001013 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001014 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001015 case R.id.add_attachment:
1016 doAttach();
1017 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001018 }
1019 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001020
1021 @Override
1022 public boolean onCreateOptionsMenu(Menu menu) {
1023 super.onCreateOptionsMenu(menu);
1024 MenuInflater inflater = getMenuInflater();
1025 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001026 mSave = menu.findItem(R.id.save);
1027 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001028 return true;
1029 }
1030
1031 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001032 public boolean onPrepareOptionsMenu(Menu menu) {
1033 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001034 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001035 // Its possible there is a menu item OR a button.
1036 boolean ccFieldVisible = mCc.isShown();
1037 boolean bccFieldVisible = mBcc.isShown();
1038 if (!ccFieldVisible || !bccFieldVisible) {
1039 ccBcc.setVisible(true);
1040 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1041 : R.string.add_bcc_label));
1042 } else {
1043 ccBcc.setVisible(false);
1044 }
1045 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001046 if (mSave != null) {
1047 mSave.setEnabled(shouldSave());
1048 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001049 return true;
1050 }
1051
1052 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001053 public boolean onOptionsItemSelected(MenuItem item) {
1054 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001055 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001056 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001057 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -08001058 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001059 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001060 case R.id.add_cc_bcc:
1061 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001062 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001063 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001064 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001065 break;
1066 case R.id.send:
1067 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001068 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001069 case R.id.discard:
1070 doDiscard();
1071 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001072 case R.id.settings:
1073 Utils.showSettings(this, mAccount);
1074 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001075 case android.R.id.home:
1076 finish();
1077 break;
1078 case R.id.help_info_menu_item:
1079 // TODO: enable context sensitive help
1080 Utils.showHelp(this, mAccount.helpIntentUri, null);
1081 break;
1082 case R.id.feedback_menu_item:
1083 Utils.sendFeedback(this, mAccount);
1084 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001085 default:
1086 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001087 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001088 }
1089 return !handled ? super.onOptionsItemSelected(item) : handled;
1090 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001091
Mindy Pereira33fe9082012-01-09 16:24:30 -08001092 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001093 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001094 }
1095
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001096 private void doSave(boolean showToast, boolean resetIME) {
1097 sendOrSaveWithSanityChecks(true, showToast, false);
1098 if (resetIME) {
1099 // Clear the IME composing suggestions from the body.
1100 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1101 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001102 }
1103
Mindy Pereira82cc5662012-01-09 17:29:30 -08001104 /*package*/ interface SendOrSaveCallback {
1105 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001106 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1107 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001108 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1109 }
1110
1111 /*package*/ static class SendOrSaveTask implements Runnable {
1112 private final Context mContext;
1113 private final SendOrSaveCallback mSendOrSaveCallback;
1114 @VisibleForTesting
1115 final SendOrSaveMessage mSendOrSaveMessage;
1116
1117 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1118 SendOrSaveCallback callback) {
1119 mContext = context;
1120 mSendOrSaveCallback = callback;
1121 mSendOrSaveMessage = message;
1122 }
1123
1124 @Override
1125 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001126 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001127
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001128 final Account selectedAccount = sendOrSaveMessage.mSelectedAccount;
1129 Message message = mSendOrSaveCallback.getMessage();
1130 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001131 // If a previous draft has been saved, in an account that is different
1132 // than what the user wants to send from, remove the old draft, and treat this
1133 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001134 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001135 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1136 ContentResolver resolver = mContext.getContentResolver();
1137 ContentValues values = new ContentValues();
1138 values.put(BaseColumns._ID, messageId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001139 if (selectedAccount.expungeMessageUri != null) {
1140 resolver.update(selectedAccount.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001141 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001142 } else {
1143 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001144 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001145 // reset messageId to 0, so a new message will be created
1146 messageId = UIProvider.INVALID_MESSAGE_ID;
1147 }
1148 }
1149
1150 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001151 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001152 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001153 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001154 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1155 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001156 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001157 ContentResolver resolver = mContext.getContentResolver();
1158 Uri messageUri = resolver.insert(
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001159 sendOrSaveMessage.mSave ? selectedAccount.saveDraftUri
1160 : selectedAccount.sendMessageUri, sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001161 if (sendOrSaveMessage.mSave && messageUri != null) {
1162 Cursor messageCursor = resolver.query(messageUri,
1163 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001164 if (messageCursor != null) {
1165 try {
1166 if (messageCursor.moveToFirst()) {
1167 // Broadcast notification that a new message has
1168 // been allocated
1169 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1170 new Message(messageCursor));
1171 }
1172 } finally {
1173 messageCursor.close();
1174 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001175 }
1176 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001177 }
1178
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001179 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001180 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001181 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001182 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001183 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001184 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001185 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001186 }
1187 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1188 }
1189 }
1190
1191 // Array of the outstanding send or save tasks. Access is synchronized
1192 // with the object itself
1193 /* package for testing */
1194 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
1195 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001196 private String mSignature;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001197
1198 /*package*/ static class SendOrSaveMessage {
1199 final Account mAccount;
1200 final Account mSelectedAccount;
1201 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001202 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001203 final boolean mSave;
1204 final int mRequestId;
1205
1206 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001207 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001208 mAccount = account;
1209 mSelectedAccount = selectedAccount;
1210 mValues = values;
1211 mRefMessageId = refMessageId;
1212 mSave = save;
1213 mRequestId = mValues.hashCode() ^ hashCode();
1214 }
1215
1216 int requestId() {
1217 return mRequestId;
1218 }
1219 }
1220
1221 /**
1222 * Get the to recipients.
1223 */
1224 public String[] getToAddresses() {
1225 return getAddressesFromList(mTo);
1226 }
1227
1228 /**
1229 * Get the cc recipients.
1230 */
1231 public String[] getCcAddresses() {
1232 return getAddressesFromList(mCc);
1233 }
1234
1235 /**
1236 * Get the bcc recipients.
1237 */
1238 public String[] getBccAddresses() {
1239 return getAddressesFromList(mBcc);
1240 }
1241
1242 public String[] getAddressesFromList(RecipientEditTextView list) {
1243 if (list == null) {
1244 return new String[0];
1245 }
1246 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1247 int count = tokens.length;
1248 String[] result = new String[count];
1249 for (int i = 0; i < count; i++) {
1250 result[i] = tokens[i].toString();
1251 }
1252 return result;
1253 }
1254
1255 /**
1256 * Check for invalid email addresses.
1257 * @param to String array of email addresses to check.
1258 * @param wrongEmailsOut Emails addresses that were invalid.
1259 */
1260 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
1261 for (String email : to) {
1262 if (!mValidator.isValid(email)) {
1263 wrongEmailsOut.add(email);
1264 }
1265 }
1266 }
1267
1268 /**
1269 * Show an error because the user has entered an invalid recipient.
1270 * @param message
1271 */
1272 public void showRecipientErrorDialog(String message) {
1273 // Only 1 invalid recipients error dialog should be allowed up at a
1274 // time.
1275 if (mRecipientErrorDialog != null) {
1276 mRecipientErrorDialog.dismiss();
1277 }
1278 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1279 R.string.recipient_error_dialog_title)
1280 .setIconAttribute(android.R.attr.alertDialogIcon)
1281 .setCancelable(false)
1282 .setPositiveButton(
1283 R.string.ok, new Dialog.OnClickListener() {
1284 public void onClick(DialogInterface dialog, int which) {
1285 // after the user dismisses the recipient error
1286 // dialog we want to make sure to refocus the
1287 // recipient to field so they can fix the issue
1288 // easily
1289 if (mTo != null) {
1290 mTo.requestFocus();
1291 }
1292 mRecipientErrorDialog = null;
1293 }
1294 }).show();
1295 }
1296
1297 /**
1298 * Update the state of the UI based on whether or not the current draft
1299 * needs to be saved and the message is not empty.
1300 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001301 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001302 if (mSave != null) {
1303 mSave.setEnabled((shouldSave() && !isBlank()));
1304 }
1305 }
1306
1307 /**
1308 * Returns true if we need to save the current draft.
1309 */
1310 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001311 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001312 // The message should only be saved if:
1313 // It hasn't been sent AND
1314 // Some text has been added to the message OR
1315 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001316 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -08001317 (mReplyFromChanged && !isBlank()));
1318 }
1319 }
1320
1321 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001322 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001323 * @return boolean
1324 */
1325 public boolean isBlank() {
1326 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001327 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1328 mBodyView.getText().toString()) == 0)
1329 && mTo.length() == 0
1330 && mCc.length() == 0 && mBcc.length() == 0
1331 && mAttachmentsView.getAttachments().size() == 0;
1332 }
1333
1334 @VisibleForTesting
1335 protected int getSignatureStartPosition(String signature, String bodyText) {
1336 int startPos = -1;
1337
1338 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1339 return startPos;
1340 }
1341
1342 int bodyLength = bodyText.length();
1343 int signatureLength = signature.length();
1344 String printableVersion = convertToPrintableSignature(signature);
1345 int printableLength = printableVersion.length();
1346
1347 if (bodyLength >= printableLength
1348 && bodyText.substring(bodyLength - printableLength)
1349 .equals(printableVersion)) {
1350 startPos = bodyLength - printableLength;
1351 } else if (bodyLength >= signatureLength
1352 && bodyText.substring(bodyLength - signatureLength)
1353 .equals(signature)) {
1354 startPos = bodyLength - signatureLength;
1355 }
1356 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001357 }
1358
1359 /**
1360 * Allows any changes made by the user to be ignored. Called when the user
1361 * decides to discard a draft.
1362 */
1363 private void discardChanges() {
1364 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001365 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001366 mReplyFromChanged = false;
1367 }
1368
1369 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001370 * @param body
1371 * @param save
1372 * @param showToast
1373 * @return Whether the send or save succeeded.
1374 */
1375 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1376 final boolean orientationChanged) {
1377 String[] to, cc, bcc;
1378 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001379
Mindy Pereira181df782012-03-01 13:32:44 -08001380 if (orientationChanged) {
1381 to = cc = bcc = new String[0];
1382 } else {
1383 to = getToAddresses();
1384 cc = getCcAddresses();
1385 bcc = getBccAddresses();
1386 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001387
Mindy Pereira181df782012-03-01 13:32:44 -08001388 // Don't let the user send to nobody (but it's okay to save a message
1389 // with no recipients)
1390 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1391 showRecipientErrorDialog(getString(R.string.recipient_needed));
1392 return false;
1393 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001394
Mindy Pereira181df782012-03-01 13:32:44 -08001395 List<String> wrongEmails = new ArrayList<String>();
1396 if (!save) {
1397 checkInvalidEmails(to, wrongEmails);
1398 checkInvalidEmails(cc, wrongEmails);
1399 checkInvalidEmails(bcc, wrongEmails);
1400 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001401
Mindy Pereira181df782012-03-01 13:32:44 -08001402 // Don't let the user send an email with invalid recipients
1403 if (wrongEmails.size() > 0) {
1404 String errorText = String.format(getString(R.string.invalid_recipient),
1405 wrongEmails.get(0));
1406 showRecipientErrorDialog(errorText);
1407 return false;
1408 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001409
Mindy Pereira181df782012-03-01 13:32:44 -08001410 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
1411 public void onClick(DialogInterface dialog, int which) {
1412 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1413 }
1414 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001415
Mindy Pereira181df782012-03-01 13:32:44 -08001416 // Show a warning before sending only if there are no attachments.
1417 if (!save) {
1418 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1419 boolean warnAboutEmptySubject = isSubjectEmpty();
1420 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001421
Mindy Pereira181df782012-03-01 13:32:44 -08001422 // A warning about an empty body may not be warranted when
1423 // forwarding mails, since a common use case is to forward
1424 // quoted text and not append any more text.
1425 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001426
Mindy Pereira181df782012-03-01 13:32:44 -08001427 // When we bring up a dialog warning the user about a send,
1428 // assume that they accept sending the message. If they do not,
1429 // the dialog listener is required to enable sending again.
1430 if (warnAboutEmptySubject) {
1431 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1432 return true;
1433 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001434
Mindy Pereira181df782012-03-01 13:32:44 -08001435 if (warnAboutEmptyBody) {
1436 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1437 return true;
1438 }
1439 }
1440 // Ask for confirmation to send (if always required)
1441 if (showSendConfirmation()) {
1442 showSendConfirmDialog(R.string.confirm_send_message, listener);
1443 return true;
1444 }
1445 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001446
Mindy Pereira181df782012-03-01 13:32:44 -08001447 sendOrSave(body, save, showToast, false);
1448 return true;
1449 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001450
Mindy Pereira181df782012-03-01 13:32:44 -08001451 /**
1452 * Returns a boolean indicating whether warnings should be shown for empty
1453 * subject and body fields
1454 *
1455 * @return True if a warning should be shown for empty text fields
1456 */
1457 protected boolean showEmptyTextWarnings() {
1458 return mAttachmentsView.getAttachments().size() == 0;
1459 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001460
Mindy Pereira181df782012-03-01 13:32:44 -08001461 /**
1462 * Returns a boolean indicating whether the user should confirm each send
1463 *
1464 * @return True if a warning should be on each send
1465 */
1466 protected boolean showSendConfirmation() {
1467 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1468 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001469
Mindy Pereira181df782012-03-01 13:32:44 -08001470 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1471 if (mSendConfirmDialog != null) {
1472 mSendConfirmDialog.dismiss();
1473 mSendConfirmDialog = null;
1474 }
1475 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1476 .setTitle(R.string.confirm_send_title)
1477 .setIconAttribute(android.R.attr.alertDialogIcon)
1478 .setPositiveButton(R.string.send, listener)
1479 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1480 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001481
Mindy Pereira181df782012-03-01 13:32:44 -08001482 /**
1483 * Returns whether the ComposeArea believes there is any text in the body of
1484 * the composition. TODO: When ComposeArea controls the Body as well, add
1485 * that here.
1486 */
1487 public boolean isBodyEmpty() {
1488 return !mQuotedTextView.isTextIncluded();
1489 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001490
Mindy Pereira181df782012-03-01 13:32:44 -08001491 /**
1492 * Test to see if the subject is empty.
1493 *
1494 * @return boolean.
1495 */
1496 // TODO: this will likely go away when composeArea.focus() is implemented
1497 // after all the widget control is moved over.
1498 public boolean isSubjectEmpty() {
1499 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1500 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001501
Mindy Pereira181df782012-03-01 13:32:44 -08001502 /* package */
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001503 static int sendOrSaveInternal(Context context, final Account account,
1504 final Account selectedAccount, String fromAddress, final Spanned body,
1505 final String[] to, final String[] cc, final String[] bcc, final String subject,
1506 final CharSequence quotedText, final List<Attachment> attachments,
Mindy Pereirac2031972012-04-03 09:38:35 -07001507 final Message refMessage, SendOrSaveCallback callback, Handler handler, boolean save,
Mindy Pereira12575862012-03-21 16:30:54 -07001508 int composeMode) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001509 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001510
Mindy Pereirac2031972012-04-03 09:38:35 -07001511 String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
1512
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001513 MessageModification.putToAddresses(values, to);
1514 MessageModification.putCcAddresses(values, cc);
1515 MessageModification.putBccAddresses(values, bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001516
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001517 MessageModification.putSubject(values, subject);
1518 String htmlBody = Html.toHtml(body);
1519 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1520 StringBuilder fullBody = new StringBuilder(htmlBody);
1521 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001522 // HTML gets converted to text for now
1523 final String text = quotedText.toString();
1524 if (QuotedTextView.containsQuotedText(text)) {
1525 int pos = QuotedTextView.getQuotedTextOffset(text);
1526 fullBody.append(text.substring(0, pos));
Paul Westbrookffe6bb02012-03-27 11:50:12 -07001527 MessageModification.putQuoteStartPos(values, fullBody.length());
Mindy Pereira12575862012-03-21 16:30:54 -07001528 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07001529 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001530 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001531 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1532 // This shouldn't happen, but just use what we have,
1533 // and don't do server-side expansion
1534 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001535 }
1536 }
Mindy Pereira12575862012-03-21 16:30:54 -07001537 int draftType = -1;
1538 switch (composeMode) {
1539 case ComposeActivity.COMPOSE:
1540 draftType = DraftType.COMPOSE;
1541 break;
1542 case ComposeActivity.REPLY:
1543 draftType = DraftType.REPLY;
1544 break;
1545 case ComposeActivity.REPLY_ALL:
1546 draftType = DraftType.REPLY_ALL;
1547 break;
1548 case ComposeActivity.FORWARD:
1549 draftType = DraftType.FORWARD;
1550 break;
1551 }
1552 MessageModification.putDraftType(values, draftType);
Mindy Pereirac2031972012-04-03 09:38:35 -07001553 if (refMessage != null && !TextUtils.isEmpty(refMessage.bodyHtml)) {
1554 MessageModification.putBodyHtml(values, fullBody.toString());
1555 }
1556 if (refMessage != null && !TextUtils.isEmpty(refMessage.bodyText)) {
1557 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1558 }
Mindy Pereiraf944e962012-01-17 11:43:36 -08001559 MessageModification.putAttachments(values, attachments);
Mindy Pereira12575862012-03-21 16:30:54 -07001560 if (!TextUtils.isEmpty(refMessageId)) {
1561 MessageModification.putRefMessageId(values, refMessageId);
1562 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001563
Mindy Pereira181df782012-03-01 13:32:44 -08001564 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1565 values, refMessageId, save);
1566 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001567
Mindy Pereira181df782012-03-01 13:32:44 -08001568 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001569
Mindy Pereira181df782012-03-01 13:32:44 -08001570 // Do the send/save action on the specified handler to avoid possible
1571 // ANRs
1572 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001573
Mindy Pereira181df782012-03-01 13:32:44 -08001574 return sendOrSaveMessage.requestId();
1575 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001576
Mindy Pereira181df782012-03-01 13:32:44 -08001577 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1578 boolean orientationChanged) {
1579 // Check if user is a monkey. Monkeys can compose and hit send
1580 // button but are not allowed to send anything off the device.
1581 if (!save && ActivityManager.isUserAMonkey()) {
1582 return;
1583 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001584
Mindy Pereira181df782012-03-01 13:32:44 -08001585 String[] to, cc, bcc;
1586 if (orientationChanged) {
1587 to = cc = bcc = new String[0];
1588 } else {
1589 to = getToAddresses();
1590 cc = getCcAddresses();
1591 bcc = getBccAddresses();
1592 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001593
Mindy Pereira181df782012-03-01 13:32:44 -08001594 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001595 private int mRestoredRequestId;
1596
1597 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08001598 synchronized (mActiveTasks) {
1599 int numTasks = mActiveTasks.size();
1600 if (numTasks == 0) {
1601 // Start service so we won't be killed if this app is
1602 // put in the background.
1603 startService(new Intent(ComposeActivity.this, EmptyService.class));
1604 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001605
Mindy Pereira181df782012-03-01 13:32:44 -08001606 mActiveTasks.add(sendOrSaveTask);
1607 }
1608 if (sTestSendOrSaveCallback != null) {
1609 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1610 }
1611 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001612
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001613 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1614 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08001615 synchronized (mDraftLock) {
1616 mDraftId = message.id;
1617 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001618 if (sRequestMessageIdMap != null) {
1619 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1620 }
Mindy Pereira181df782012-03-01 13:32:44 -08001621 // Cache request message map, in case the process is killed
1622 saveRequestMap();
1623 }
1624 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001625 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08001626 }
1627 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001628
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001629 public Message getMessage() {
1630 synchronized (mDraftLock) {
1631 return mDraft;
1632 }
1633 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001634
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001635 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1636 if (success) {
1637 // Successfully sent or saved so reset change markers
1638 discardChanges();
1639 } else {
1640 // A failure happened with saving/sending the draft
1641 // TODO(pwestbro): add a better string that should be used
1642 // when failing to send or save
1643 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1644 .show();
1645 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001646
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001647 int numTasks;
1648 synchronized (mActiveTasks) {
1649 // Remove the task from the list of active tasks
1650 mActiveTasks.remove(task);
1651 numTasks = mActiveTasks.size();
1652 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001653
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001654 if (numTasks == 0) {
1655 // Stop service so we can be killed.
1656 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1657 }
1658 if (sTestSendOrSaveCallback != null) {
1659 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1660 }
1661 }
Mindy Pereira181df782012-03-01 13:32:44 -08001662 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001663
Mindy Pereira181df782012-03-01 13:32:44 -08001664 // Get the selected account if the from spinner has been setup.
1665 Account selectedAccount = mAccount;
1666 String fromAddress = selectedAccount.name;
1667 if (selectedAccount == null || fromAddress == null) {
1668 // We don't have either the selected account or from address,
1669 // use mAccount.
1670 selectedAccount = mAccount;
1671 fromAddress = mAccount.name;
1672 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001673
Mindy Pereira181df782012-03-01 13:32:44 -08001674 if (mSendSaveTaskHandler == null) {
1675 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1676 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001677
Mindy Pereira181df782012-03-01 13:32:44 -08001678 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1679 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001680
Mindy Pereira181df782012-03-01 13:32:44 -08001681 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body, to, cc,
Paul Westbrookffe6bb02012-03-27 11:50:12 -07001682 bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedTextIfIncluded(),
Mindy Pereirac2031972012-04-03 09:38:35 -07001683 mAttachmentsView.getAttachments(), mRefMessage, callback,
Mindy Pereira12575862012-03-21 16:30:54 -07001684 mSendSaveTaskHandler, save, mComposeMode);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001685
Mindy Pereira181df782012-03-01 13:32:44 -08001686 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1687 mRecipient = selectedAccount.name;
1688 }
1689 mAccount = selectedAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001690
Mindy Pereira181df782012-03-01 13:32:44 -08001691 // Don't display the toast if the user is just changing the orientation,
1692 // but we still need to save the draft to the cursor because this is how we restore
1693 // the attachments when the configuration change completes.
1694 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1695 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1696 Toast.LENGTH_LONG).show();
1697 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001698
Mindy Pereira181df782012-03-01 13:32:44 -08001699 // Need to update variables here because the send or save completes
1700 // asynchronously even though the toast shows right away.
1701 discardChanges();
1702 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001703
Mindy Pereira181df782012-03-01 13:32:44 -08001704 // If we are sending, finish the activity
1705 if (!save) {
1706 finish();
1707 }
1708 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001709
Mindy Pereira181df782012-03-01 13:32:44 -08001710 /**
1711 * Save the state of the request messageid map. This allows for the Gmail
1712 * process to be killed, but and still allow for ComposeActivity instances
1713 * to be recreated correctly.
1714 */
1715 private void saveRequestMap() {
1716 // TODO: store the request map in user preferences.
1717 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001718
Mindy Pereira013194c2012-01-06 15:09:33 -08001719 public void doAttach() {
1720 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1721 i.addCategory(Intent.CATEGORY_OPENABLE);
Mindy Pereira181df782012-03-01 13:32:44 -08001722 if (android.provider.Settings.System.getInt(getContentResolver(),
1723 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08001724 i.setType("*/*");
1725 } else {
1726 i.setType("image/*");
1727 }
1728 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08001729 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
1730 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08001731 }
1732
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001733 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001734 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001735 if (mCcBccButton != null) {
1736 mCcBccButton.setVisibility(View.GONE);
1737 }
1738 }
1739
Mindy Pereira326c6602012-01-04 15:32:42 -08001740 @Override
1741 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001742 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001743 if (position == ComposeActivity.REPLY) {
1744 mComposeMode = ComposeActivity.REPLY;
1745 } else if (position == ComposeActivity.REPLY_ALL) {
1746 mComposeMode = ComposeActivity.REPLY_ALL;
1747 } else if (position == ComposeActivity.FORWARD) {
1748 mComposeMode = ComposeActivity.FORWARD;
1749 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001750 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001751 resetMessageForModeChange();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001752 if (mRefMessage != null) {
1753 initFromRefMessage(mComposeMode, mAccount.name);
1754 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001755 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001756 return true;
1757 }
1758
Mindy Pereira154386a2012-01-11 13:02:33 -08001759 private void resetMessageForModeChange() {
1760 // When switching between reply, reply all, forward,
1761 // follow the behavior of webview.
1762 // The contents of the following fields are cleared
1763 // so that they can be populated directly from the
1764 // ref message:
1765 // 1) Any recipient fields
1766 // 2) The subject
1767 mTo.setText("");
1768 mCc.setText("");
1769 mBcc.setText("");
1770 // Any edits to the subject are replaced with the original subject.
1771 mSubject.setText("");
1772
1773 // Any changes to the contents of the following fields are kept:
1774 // 1) Body
1775 // 2) Attachments
1776 // If the user made changes to attachments, keep their changes.
1777 if (!mAttachmentsChanged) {
1778 mAttachmentsView.deleteAllAttachments();
1779 }
1780 }
1781
Mindy Pereira326c6602012-01-04 15:32:42 -08001782 private class ComposeModeAdapter extends ArrayAdapter<String> {
1783
1784 private LayoutInflater mInflater;
1785
1786 public ComposeModeAdapter(Context context) {
1787 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1788 .getStringArray(R.array.compose_modes));
1789 }
1790
1791 private LayoutInflater getInflater() {
1792 if (mInflater == null) {
1793 mInflater = LayoutInflater.from(getContext());
1794 }
1795 return mInflater;
1796 }
1797
1798 @Override
1799 public View getView(int position, View convertView, ViewGroup parent) {
1800 if (convertView == null) {
1801 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1802 }
1803 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1804 return super.getView(position, convertView, parent);
1805 }
1806 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001807
1808 @Override
1809 public void onRespondInline(String text) {
1810 appendToBody(text, false);
1811 }
1812
1813 /**
1814 * Append text to the body of the message. If there is no existing body
1815 * text, just sets the body to text.
1816 *
1817 * @param text
1818 * @param withSignature True to append a signature.
1819 */
1820 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001821 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001822 if (bodyText != null && bodyText.length() > 0) {
1823 bodyText.append(text);
1824 } else {
1825 setBody(text, withSignature);
1826 }
1827 }
1828
1829 /**
1830 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001831 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001832 * @param text
1833 * @param withSignature True to append a signature.
1834 */
1835 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001836 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001837 if (withSignature) {
1838 appendSignature();
1839 }
1840 }
1841
1842 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07001843 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07001844 boolean hasFocus = mBodyView.hasFocus();
Mindy Pereirab13917c2012-03-29 08:08:19 -07001845 if (!TextUtils.equals(newSignature, mSignature)) {
1846 mSignature = newSignature;
1847 if (!TextUtils.isEmpty(mSignature)
1848 && getSignatureStartPosition(mSignature,
1849 mBodyView.getText().toString()) < 0) {
1850 // Appending a signature does not count as changing text.
1851 mBodyView.removeTextChangedListener(this);
1852 mBodyView.append(convertToPrintableSignature(mSignature));
1853 mBodyView.addTextChangedListener(this);
1854 }
Mindy Pereira433b1982012-04-03 11:53:07 -07001855 if (hasFocus) {
1856 focusBody();
1857 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001858 }
1859 }
1860
1861 private String convertToPrintableSignature(String signature) {
1862 String signatureResource = getResources().getString(R.string.signature);
1863 if (signature == null) {
1864 signature = "";
1865 }
1866 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001867 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001868
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001869 @Override
1870 public void onAccountChanged() {
1871 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereira181df782012-03-01 13:32:44 -08001872 if (!mAccount.equals(selectedAccountInfo)) {
1873 mAccount = selectedAccountInfo;
1874 mCachedSettings = null;
1875 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
1876 // TODO: handle discarding attachments when switching accounts.
1877 // Only enable save for this draft if there is any other content
1878 // in the message.
1879 if (!isBlank()) {
1880 enableSave(true);
1881 }
1882 mReplyFromChanged = true;
1883 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001884 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001885 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001886
1887 public void enableSave(boolean enabled) {
1888 if (mSave != null) {
1889 mSave.setEnabled(enabled);
1890 }
1891 }
1892
1893 public void enableSend(boolean enabled) {
1894 if (mSend != null) {
1895 mSend.setEnabled(enabled);
1896 }
1897 }
1898
1899 /**
1900 * Handles button clicks from any error dialogs dealing with sending
1901 * a message.
1902 */
1903 @Override
1904 public void onClick(DialogInterface dialog, int which) {
1905 switch (which) {
1906 case DialogInterface.BUTTON_POSITIVE: {
1907 doDiscardWithoutConfirmation(true /* show toast */ );
1908 break;
1909 }
1910 case DialogInterface.BUTTON_NEGATIVE: {
1911 // If the user cancels the send, re-enable the send button.
1912 enableSend(true);
1913 break;
1914 }
1915 }
1916
1917 }
1918
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001919 private void doDiscard() {
1920 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
1921 .setPositiveButton(R.string.ok, this)
1922 .setNegativeButton(R.string.cancel, null)
1923 .create().show();
1924 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001925 /**
1926 * Effectively discard the current message.
1927 *
1928 * This method is either invoked from the menu or from the dialog
1929 * once the user has confirmed that they want to discard the message.
1930 * @param showToast show "Message discarded" toast if true
1931 */
1932 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001933 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001934 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1935 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07001936 values.put(BaseColumns._ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001937 if (mAccount.expungeMessageUri != null) {
1938 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
1939 } else {
1940 // TODO(mindyp): call delete on this conversation instead.
1941 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001942 // This is not strictly necessary (since we should not try to
1943 // save the draft after calling this) but it ensures that if we
1944 // do save again for some reason we make a new draft rather than
1945 // trying to resave an expunged draft.
1946 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1947 }
1948 }
1949
1950 if (showToast) {
1951 // Display a toast to let the user know
1952 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1953 }
1954
1955 // This prevents the draft from being saved in onPause().
1956 discardChanges();
1957 finish();
1958 }
1959
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001960 private void saveIfNeeded() {
1961 if (mAccount == null) {
1962 // We have not chosen an account yet so there's no way that we can save. This is ok,
1963 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1964 // user has not interacted with us yet and there is no real state to save.
1965 return;
1966 }
1967
1968 if (shouldSave()) {
1969 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1970 }
1971 }
1972
1973 private void saveIfNeededOnOrientationChanged() {
1974 if (mAccount == null) {
1975 // We have not chosen an account yet so there's no way that we can save. This is ok,
1976 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1977 // user has not interacted with us yet and there is no real state to save.
1978 return;
1979 }
1980
1981 if (shouldSave()) {
1982 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1983 }
1984 }
1985
1986 /**
1987 * Save a draft if a draft already exists or the message is not empty.
1988 */
1989 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1990 saveOnOrientationChanged();
1991 if (resetIME) {
1992 // Clear the IME composing suggestions from the body.
1993 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1994 }
1995 }
1996
1997 protected boolean saveOnOrientationChanged() {
1998 return sendOrSaveWithSanityChecks(true, false, true);
1999 }
2000
2001 @Override
2002 public void onAttachmentDeleted() {
2003 mAttachmentsChanged = true;
2004 updateSaveUi();
2005 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002006
2007
2008 /**
2009 * This is called any time one of our text fields changes.
2010 */
2011 public void afterTextChanged(Editable s) {
2012 mTextChanged = true;
2013 updateSaveUi();
2014 }
2015
2016 @Override
2017 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2018 // Do nothing.
2019 }
2020
2021 public void onTextChanged(CharSequence s, int start, int before, int count) {
2022 // Do nothing.
2023 }
2024
2025
2026 // There is a big difference between the text associated with an address changing
2027 // to add the display name or to format properly and a recipient being added or deleted.
2028 // Make sure we only notify of changes when a recipient has been added or deleted.
2029 private class RecipientTextWatcher implements TextWatcher {
2030 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2031
2032 private RecipientEditTextView mView;
2033
2034 private TextWatcher mListener;
2035
2036 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2037 mView = view;
2038 mListener = listener;
2039 }
2040
2041 @Override
2042 public void afterTextChanged(Editable s) {
2043 if (hasChanged()) {
2044 mListener.afterTextChanged(s);
2045 }
2046 }
2047
2048 private boolean hasChanged() {
2049 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2050 int totalCount = currRecips.length;
2051 int totalPrevCount = 0;
2052 for (Entry<String, Integer> entry : mContent.entrySet()) {
2053 totalPrevCount += entry.getValue();
2054 }
2055 if (totalCount != totalPrevCount) {
2056 return true;
2057 }
2058
2059 for (String recip : currRecips) {
2060 if (!mContent.containsKey(recip)) {
2061 return true;
2062 } else {
2063 int count = mContent.get(recip) - 1;
2064 if (count < 0) {
2065 return true;
2066 } else {
2067 mContent.put(recip, count);
2068 }
2069 }
2070 }
2071 return false;
2072 }
2073
2074 private String[] tokenizeRecips(String[] recips) {
2075 // Tokenize them all and put them in the list.
2076 String[] recipAddresses = new String[recips.length];
2077 for (int i = 0; i < recips.length; i++) {
2078 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2079 }
2080 return recipAddresses;
2081 }
2082
2083 @Override
2084 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2085 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2086 for (String recip : recips) {
2087 if (!mContent.containsKey(recip)) {
2088 mContent.put(recip, 1);
2089 } else {
2090 mContent.put(recip, (mContent.get(recip)) + 1);
2091 }
2092 }
2093 }
2094
2095 @Override
2096 public void onTextChanged(CharSequence s, int start, int before, int count) {
2097 // Do nothing.
2098 }
2099 }
Mindy Pereira181df782012-03-01 13:32:44 -08002100
2101 @Override
2102 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
2103 if (id == ACCOUNT_SETTINGS_LOADER) {
Mindy Pereira23e9fde2012-03-20 15:08:24 -07002104 if (mAccount != null && mAccount.settingsQueryUri != null) {
Mindy Pereira181df782012-03-01 13:32:44 -08002105 return new CursorLoader(this, mAccount.settingsQueryUri,
2106 UIProvider.SETTINGS_PROJECTION, null, null, null);
2107 }
2108 }
2109 return null;
2110 }
2111
2112 @Override
2113 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
2114 if (loader.getId() == ACCOUNT_SETTINGS_LOADER) {
2115 if (data != null) {
2116 data.moveToFirst();
2117 mCachedSettings = new Settings(data);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002118 appendSignature();
Mindy Pereira181df782012-03-01 13:32:44 -08002119 }
2120 }
2121 }
2122
2123 @Override
2124 public void onLoaderReset(Loader<Cursor> loader) {
2125 // Do nothing.
2126 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002127}