blob: d18a718afaa7a0373f1224d75b6e89f0933afb5f [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 Pereira1f936682012-03-02 11:30:33 -080059import android.widget.ImageView;
Mindy Pereira6349a042012-01-04 11:25:01 -080060import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080061import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080062
Mindy Pereirac17d0732011-12-29 10:46:19 -080063import com.android.common.Rfc822Validator;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080064import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080065import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080066import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080067import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080068import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080069import com.android.mail.providers.Address;
70import com.android.mail.providers.Attachment;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080071import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080072import com.android.mail.providers.MessageModification;
Mindy Pereira181df782012-03-01 13:32:44 -080073import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080074import com.android.mail.providers.UIProvider;
Andy Huang30e2c242012-01-06 18:14:30 -080075import com.android.mail.R;
Paul Westbrook92227f62012-03-20 10:32:51 -070076import com.android.mail.utils.AccountUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080077import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080078import com.android.mail.utils.Utils;
Mindy Pereirac17d0732011-12-29 10:46:19 -080079import com.android.ex.chips.RecipientEditTextView;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080080import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080081import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080082import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080083
Mindy Pereira8eca57a2012-03-20 16:42:34 -070084import java.io.UnsupportedEncodingException;
85import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080086import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070087import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080088import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080089import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080090import java.util.HashSet;
91import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -070092import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070093import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -080094import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080095
96public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080097 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Mindy Pereira181df782012-03-01 13:32:44 -080098 AttachmentDeletedListener, OnAccountChangedListener, LoaderCallbacks<Cursor> {
Mindy Pereira6349a042012-01-04 11:25:01 -080099 // Identifiers for which type of composition this is
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700100 static final int COMPOSE = -1;
Mindy Pereira6349a042012-01-04 11:25:01 -0800101 static final int REPLY = 0;
102 static final int REPLY_ALL = 1;
103 static final int FORWARD = 2;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700104 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800105
106 // Integer extra holding one of the above compose action
107 private static final String EXTRA_ACTION = "action";
108
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700109 private static final String UTF8_ENCODING_NAME = "UTF-8";
110
111 private static final String MAIL_TO = "mailto";
112
113 private static final String GMAIL_FROM = "gmailfrom";
114
115 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 Pereiraeaea9f12012-01-10 15:05:27 -0800190 private TextView 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 Pereira3ce64e72012-01-13 14:29:45 -0800201 private String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800202 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800203 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800204 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800205 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
206 private Message mDraft;
207 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800208 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800209
Mindy Pereira326c6602012-01-04 15:32:42 -0800210 /**
211 * Can be called from a non-UI thread.
212 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800213 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700214 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800215 }
216
Mindy Pereira6349a042012-01-04 11:25:01 -0800217 /**
218 * Can be called from a non-UI thread.
219 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800220 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800221 launch(launcher, account, null, COMPOSE);
222 }
223
224 /**
225 * Can be called from a non-UI thread.
226 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800227 public static void reply(Context launcher, Account account, Message message) {
228 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800229 }
230
231 /**
232 * Can be called from a non-UI thread.
233 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800234 public static void replyAll(Context launcher, Account account, Message message) {
235 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800236 }
237
238 /**
239 * Can be called from a non-UI thread.
240 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800241 public static void forward(Context launcher, Account account, Message message) {
242 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800243 }
244
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800245 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800246 Intent intent = new Intent(launcher, ComposeActivity.class);
247 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
248 intent.putExtra(EXTRA_ACTION, action);
249 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700250 if (action == EDIT_DRAFT) {
251 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
252 } else {
253 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
254 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800255 launcher.startActivity(intent);
256 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800257
258 @Override
259 public void onCreate(Bundle savedInstanceState) {
260 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800261 setContentView(R.layout.compose);
262 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800263 Intent intent = getIntent();
Paul Westbrook92227f62012-03-20 10:32:51 -0700264
265 Account account = (Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
266 if (account == null) {
267 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
268 if (syncingAccounts.length > 0) {
269 account = syncingAccounts[0];
270 }
271 }
272
273 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800274 if (mAccount == null) {
275 return;
276 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800277 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800278 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereira29ef1b82012-01-13 11:26:21 -0800279 if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700280 if (mRefMessage != null) {
281 initFromRefMessage(action, mAccount.name);
282 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700283 } else if (action == EDIT_DRAFT) {
284 // Initialize the message from the message in the intent
285 final Message message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
286
287 initFromMessage(message);
288
289 // Update the action to the draft type of the previous draft
290 switch (message.draftType) {
291 case UIProvider.DraftType.REPLY:
292 action = REPLY;
293 break;
294 case UIProvider.DraftType.REPLY_ALL:
295 action = REPLY_ALL;
296 break;
297 case UIProvider.DraftType.FORWARD:
298 action = FORWARD;
299 break;
300 case UIProvider.DraftType.COMPOSE:
301 default:
302 action = COMPOSE;
303 break;
304 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700305 } else {
306 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700307 }
308
309 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800310 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800311 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800312 initRecipients();
Paul Westbrookf97588b2012-03-20 11:11:37 -0700313 initAttachmentsFromIntent(intent);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800314 initActionBar(action);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800315 initFromSpinner();
Mindy Pereira75f66632012-01-11 11:42:02 -0800316 initChangeListeners();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800317 }
318
319 @Override
320 protected void onResume() {
321 super.onResume();
322 // Update the from spinner as other accounts
323 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800324 if (mFromSpinner != null && mAccount != null) {
325 mFromSpinner.asyncInitFromSpinner();
326 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800327 }
328
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800329 @Override
330 protected void onPause() {
331 super.onPause();
332
333 if (mSendConfirmDialog != null) {
334 mSendConfirmDialog.dismiss();
335 }
336 if (mRecipientErrorDialog != null) {
337 mRecipientErrorDialog.dismiss();
338 }
339
340 saveIfNeeded();
341 }
342
343 @Override
344 protected final void onActivityResult(int request, int result, Intent data) {
345 mAddingAttachment = false;
346
347 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
348 addAttachmentAndUpdateView(data);
349 }
350 }
351
352 @Override
353 public final void onSaveInstanceState(Bundle state) {
354 super.onSaveInstanceState(state);
355
356 // onSaveInstanceState is only called if the user might come back to this activity so it is
357 // not an ideal location to save the draft. However, if we have never saved the draft before
358 // we have to save it here in order to have an id to save in the bundle.
359 saveIfNeededOnOrientationChanged();
360 }
361
Mindy Pereira818143e2012-01-11 13:59:49 -0800362 @VisibleForTesting
363 void setAccount(Account account) {
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700364 assert(account != null);
365 if (!account.equals(mAccount)) {
366 mAccount = account;
367 }
Mindy Pereira181df782012-03-01 13:32:44 -0800368 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800369 }
370
Mindy Pereira1a95a572012-01-05 12:21:29 -0800371 private void initFromSpinner() {
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800372 mFromSpinner.setCurrentAccount(mAccount);
373 mFromSpinner.asyncInitFromSpinner();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800374 boolean showSpinner = mFromSpinner.getCount() > 1;
375 // If there is only 1 account, just show that account.
376 // Otherwise, give the user the ability to choose which account to send
377 // mail from / save drafts to.
378 mFromStatic.setVisibility(
379 showSpinner ? View.GONE : View.VISIBLE);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800380 mFromStaticText.setText(mAccount.name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800381 mFromSpinnerWrapper.setVisibility(
382 showSpinner ? View.VISIBLE : View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800383 }
384
385 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800386 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800387 if (mCcBccButton != null) {
388 mCcBccButton.setOnClickListener(this);
389 }
390 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800391 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800392 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
393 if (mAttachmentsButton != null) {
394 mAttachmentsButton.setOnClickListener(this);
395 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800396 mTo = (RecipientEditTextView) findViewById(R.id.to);
397 mCc = (RecipientEditTextView) findViewById(R.id.cc);
398 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800399 // TODO: add special chips text change watchers before adding
400 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800401 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800402 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
403 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800404 mBodyView = (TextView) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800405 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800406 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800407 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800408 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800409 }
410
Mindy Pereira75f66632012-01-11 11:42:02 -0800411 // Now that the message has been initialized from any existing draft or
412 // ref message data, set up listeners for any changes that occur to the
413 // message.
414 private void initChangeListeners() {
415 mSubject.addTextChangedListener(this);
416 mBodyView.addTextChangedListener(this);
417 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
418 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
419 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
420 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800421 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800422 }
423
Mindy Pereira326c6602012-01-04 15:32:42 -0800424 private void initActionBar(int action) {
425 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800426 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800427 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800428 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
429 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800430 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800431 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800432 if (mComposeModeAdapter == null) {
433 mComposeModeAdapter = new ComposeModeAdapter(this);
434 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800435 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
436 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800437 switch (action) {
438 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800439 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800440 break;
441 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800442 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800443 break;
444 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800445 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800446 break;
447 }
448 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700449 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
450 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
451 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800452 }
453
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800454 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700455 mRefMessageId = mRefMessage.refMessageId;
456 setSubject(mRefMessage, action);
457 // Setup recipients
458 if (action == FORWARD) {
459 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800460 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700461 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
462 initBodyFromRefMessage(mRefMessage, action);
463 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
464 initAttachments(mRefMessage);
465 }
466 updateHideOrShowCcBcc();
Mindy Pereirac17d0732011-12-29 10:46:19 -0800467 }
468
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700469 private void initFromMessage(Message message) {
470 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
471
472 mDraft = message;
473 mDraftId = message.id;
474 mSubject.setText(message.subject);
475 mForward = message.draftType == UIProvider.DraftType.FORWARD;
476 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
477 addToAddresses(toAddresses);
478 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
479 addBccAddresses(Arrays.asList(message.getBccAddresses()));
480
481 // Set the body
482 if (!TextUtils.isEmpty(message.bodyHtml)) {
483 mBodyView.setText(Html.fromHtml(message.bodyHtml));
484 } else {
485 mBodyView.setText(message.bodyText);
486 }
487
488 // TODO: load attachments from the previous message
489 // TODO: set the from address spinner to the right from account
490 // TODO: initialize quoted text value
491 }
492
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700493 /**
494 * Fill all the widgets with the content found in the Intent Extra, if any.
495 * Also apply the same style to all widgets. Note: if initFromExtras is
496 * called as a result of switching between reply, reply all, and forward per
497 * the latest revision of Gmail, and the user has already made changes to
498 * attachments on a previous incarnation of the message (as a reply, reply
499 * all, or forward), the original attachments from the message will not be
500 * re-instantiated. The user's changes will be respected. This follows the
501 * web gmail interaction.
502 */
503 public void initFromExtras(Intent intent) {
504
505 // If we were invoked with a SENDTO intent, the value
506 // should take precedence
507 final Uri dataUri = intent.getData();
508 if (dataUri != null) {
509 if (MAIL_TO.equals(dataUri.getScheme())) {
510 initFromMailTo(dataUri.toString());
511 } else {
512 if (!GMAIL_FROM.equals(dataUri.getScheme())) {
513 String toText = dataUri.getSchemeSpecificPart();
514 if (toText != null) {
515 mTo.setText("");
516 addToAddresses(Arrays.asList(toText.split(",")));
517 }
518 }
519 }
520 }
521
522 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
523 if (extraStrings != null) {
524 addToAddresses(Arrays.asList(extraStrings));
525 }
526 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
527 if (extraStrings != null) {
528 addCcAddresses(Arrays.asList(extraStrings), null);
529 }
530 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
531 if (extraStrings != null) {
532 addBccAddresses(Arrays.asList(extraStrings));
533 }
534
535 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
536 if (extraString != null) {
537 mSubject.setText(extraString);
538 }
539
540 for (String extra : ALL_EXTRAS) {
541 if (intent.hasExtra(extra)) {
542 String value = intent.getStringExtra(extra);
543 if (EXTRA_TO.equals(extra)) {
544 addToAddresses(Arrays.asList(value.split(",")));
545 } else if (EXTRA_CC.equals(extra)) {
546 addCcAddresses(Arrays.asList(value.split(",")), null);
547 } else if (EXTRA_BCC.equals(extra)) {
548 addBccAddresses(Arrays.asList(value.split(",")));
549 } else if (EXTRA_SUBJECT.equals(extra)) {
550 mSubject.setText(value);
551 } else if (EXTRA_BODY.equals(extra)) {
552 setBody(value, true /* with signature */);
553 }
554 }
555 }
556
557 Bundle extras = intent.getExtras();
558 if (extras != null) {
559 final String action = intent.getAction();
560 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
561 if (text != null) {
562 setBody(text, true /* with signature */);
563 }
564
565 // TODO(mindyp): read this from account settings.
566 int maxSize = DEFAULT_MAX_ATTACHMENT_SIZE;
567 int totalSize = 0;
568
569 // Take care of attachments passed in by the extras.
570 if (!mAttachmentsChanged) {
571 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
572 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
573 for (String uriString : uris) {
574 final Uri uri = Uri.parse(uriString);
575 long size;
576 try {
577 size = mAttachmentsView.addAttachment(mAccount, uri,
578 false /* doSave */, true /* local file */);
579 } catch (AttachmentFailureException e) {
580 // A toast has already been shown to the user,
581 // just break out of the loop.
582 LogUtils.e(LOG_TAG, e, "Error adding attachment");
583 finish();
584 return;
585 }
586 }
587 mAttachmentsChanged = true;
588 }
589 if ((Intent.ACTION_SEND.equals(action)
590 || AUTO_SEND_ACTION.equals(action))
591 && extras.containsKey(Intent.EXTRA_STREAM)) {
592 Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
593 try {
594 mAttachmentsView.addAttachment(mAccount, uri, true /* doSave */,
595 true /* local file */);
596 } catch (AttachmentFailureException e) {
597 // A toast has already been shown to the user, so just
598 // exit.
599 LogUtils.e(LOG_TAG, e, "Error adding attachment");
600 finish();
601 return;
602 }
603 }
604
605 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
606 && extras.containsKey(Intent.EXTRA_STREAM)) {
607 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
608 for (Parcelable uri : uris) {
609 try {
610 mAttachmentsView.addAttachment(mAccount,
611 (Uri) uri, false /* doSave */, true /* local file */);
612 } catch (AttachmentFailureException e) {
613 // A toast has already been shown to the user,
614 // just break out of the loop.
615 LogUtils.e(LOG_TAG, e, "Error adding attachment");
616 finish();
617 return;
618 }
619 }
620 mAttachmentsChanged = true;
621 }
622 }
623 }
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) {
705 final Bundle extras = intent.getExtras();
706 final String action = intent.getAction();
707 if (!mAttachmentsChanged) {
708 long totalSize = 0;
709 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
710 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
711 for (String uriString : uris) {
712 final Uri uri = Uri.parse(uriString);
713 long size = 0;
714 try {
715 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
716 true /* local file */);
717 } catch (AttachmentFailureException e) {
718 // A toast has already been shown to the user,
719 // just break out of the loop.
720 LogUtils.e(LOG_TAG, e, "Error adding attachment");
721 }
722 totalSize += size;
723 }
724 }
725 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
726 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
727 long size = 0;
728 try {
729 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
730 true /* local file */);
731 } catch (AttachmentFailureException e) {
732 // A toast has already been shown to the user, so just
733 // exit.
734 LogUtils.e(LOG_TAG, e, "Error adding attachment");
735 }
736 totalSize += size;
737 }
738
739 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
740 && extras.containsKey(Intent.EXTRA_STREAM)) {
741 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
742 for (Parcelable uri : uris) {
743 long size = 0;
744 try {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700745 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri,
Paul Westbrookf97588b2012-03-20 11:11:37 -0700746 false /* doSave */, true /* local file */);
747 } catch (AttachmentFailureException e) {
748 // A toast has already been shown to the user,
749 // just break out of the loop.
750 LogUtils.e(LOG_TAG, e, "Error adding attachment");
751 }
752 totalSize += size;
753 }
754 }
755
756 if (totalSize > 0) {
757 mAttachmentsChanged = true;
758 updateSaveUi();
759 }
760 }
761 }
762
763
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800764 private void initBodyFromRefMessage(Message refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800765 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800766 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
767 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800768 }
769
770 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800771 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800772 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
773 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
774 if (ccVisible || bccVisible) {
775 mCcBccView.show(false, ccVisible, bccVisible);
776 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800777 if (mCcBccButton != null) {
778 if (!mCc.isShown() || !mBcc.isShown()) {
779 mCcBccButton.setVisibility(View.VISIBLE);
780 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
781 : R.string.add_bcc_label));
782 } else {
783 mCcBccButton.setVisibility(View.GONE);
784 }
785 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800786 }
787
Mindy Pereira013194c2012-01-06 15:09:33 -0800788 /**
789 * Add attachment and update the compose area appropriately.
790 * @param data
791 */
792 public void addAttachmentAndUpdateView(Intent data) {
793 Uri uri = data != null ? data.getData() : null;
Mindy Pereira013194c2012-01-06 15:09:33 -0800794 try {
Mindy Pereiraf944e962012-01-17 11:43:36 -0800795 long size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
796 true /* local file */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800797 if (size > 0) {
798 mAttachmentsChanged = true;
799 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800800 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800801 } catch (AttachmentFailureException e) {
802 // A toast has already been shown to the user, no need to do
803 // anything.
804 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800805 }
806 }
807
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800808 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800809 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800810 // Don't populate the address if this is a forward.
811 if (action == ComposeActivity.FORWARD) {
812 return;
813 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800814 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800815 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800816
Mindy Pereira818143e2012-01-11 13:59:49 -0800817 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800818 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800819 // This is the email address of the current user, i.e. the one composing
820 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800821 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800822 String fromAddress = refMessage.from;
823 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
824 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800825 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800826
827 // If this is a reply, the Cc list is empty. If this is a reply-all, the
828 // Cc list is the union of the To and Cc recipients of the original
829 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800830 // already on the To list.
831 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800832 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
833 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800834 addToAddresses(toAddresses);
835 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800836 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800837 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
838 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800839 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800840 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800841 addRecipients(accountEmail, ccAddresses,
842 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800843 addCcAddresses(ccAddresses, toAddresses);
844 }
845 }
846
847 private void addToAddresses(Collection<String> addresses) {
848 addAddressesToList(addresses, mTo);
849 }
850
851 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700852 addCcAddressesToList(tokenizeAddressList(addresses),
853 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800854 }
855
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700856 private void addBccAddresses(Collection<String> addresses) {
857 addAddressesToList(addresses, mBcc);
858 }
859
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800860 @VisibleForTesting
861 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
862 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
863 String address;
864
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700865 if (compareToList == null) {
866 for (Rfc822Token[] tokens : addresses) {
867 for (int i = 0; i < tokens.length; i++) {
868 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800869 list.append(address + END_TOKEN);
870 }
871 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700872 } else {
873 HashSet<String> compareTo = convertToHashSet(compareToList);
874 for (Rfc822Token[] tokens : addresses) {
875 for (int i = 0; i < tokens.length; i++) {
876 address = tokens[i].toString();
877 // Check if this is a duplicate:
878 if (!compareTo.contains(tokens[i].getAddress())) {
879 // Get the address here
880 list.append(address + END_TOKEN);
881 }
882 }
883 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800884 }
885 }
886
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800887 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
888 HashSet<String> hash = new HashSet<String>();
889 for (Rfc822Token[] tokens : list) {
890 for (int i = 0; i < tokens.length; i++) {
891 hash.add(tokens[i].getAddress());
892 }
893 }
894 return hash;
895 }
896
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800897 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
898 @VisibleForTesting
899 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
900
901 for (String address: addresses) {
902 tokenized.add(Rfc822Tokenizer.tokenize(address));
903 }
904 return tokenized;
905 }
906
907 @VisibleForTesting
908 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
909 for (String address : addresses) {
910 addAddressToList(address, list);
911 }
912 }
913
914 private void addAddressToList(String address, RecipientEditTextView list) {
915 if (address == null || list == null)
916 return;
917
918 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
919
920 for (int i = 0; i < tokens.length; i++) {
921 list.append(tokens[i] + END_TOKEN);
922 }
923 }
924
925 @VisibleForTesting
926 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800927 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800928 // The To recipient is the reply-to address specified in the original
929 // message, unless it is:
930 // the current user OR a custom from of the current user, in which case
931 // it's the To recipient list of the original message.
932 // OR missing, in which case use the sender of the original message
933 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800934 if (!TextUtils.isEmpty(replyToAddress)) {
935 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800936 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800937 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800938 }
939 return toAddresses;
940 }
941
942 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
943 for (String email : addresses) {
944 // Do not add this account, or any of the custom froms, to the list
945 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800946 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800947 if (!account.equalsIgnoreCase(recipientAddress)) {
948 recipients.add(email.replace("\"\"", ""));
949 }
950 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800951 }
952
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800953 private void setSubject(Message refMessage, int action) {
954 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800955 String prefix;
956 String correctedSubject = null;
957 if (action == ComposeActivity.COMPOSE) {
958 prefix = "";
959 } else if (action == ComposeActivity.FORWARD) {
960 prefix = getString(R.string.forward_subject_label);
961 } else {
962 prefix = getString(R.string.reply_subject_label);
963 }
964
965 // Don't duplicate the prefix
966 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
967 correctedSubject = subject;
968 } else {
969 correctedSubject = String
970 .format(getString(R.string.formatted_subject), prefix, subject);
971 }
972 mSubject.setText(correctedSubject);
973 }
974
Mindy Pereira818143e2012-01-11 13:59:49 -0800975 private void initRecipients() {
976 setupRecipients(mTo);
977 setupRecipients(mCc);
978 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800979 }
980
Mindy Pereira818143e2012-01-11 13:59:49 -0800981 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800982 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800983 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800984 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800985 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800986 int offset = accountName.indexOf("@") + 1;
987 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800988 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800989 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800990 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800991 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800992 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800993 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800994 }
995
996 @Override
997 public void onClick(View v) {
998 int id = v.getId();
999 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001000 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001001 // Verify that cc/ bcc aren't showing.
1002 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001003 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001004 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001005 case R.id.add_attachment:
1006 doAttach();
1007 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001008 }
1009 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001010
1011 @Override
1012 public boolean onCreateOptionsMenu(Menu menu) {
1013 super.onCreateOptionsMenu(menu);
1014 MenuInflater inflater = getMenuInflater();
1015 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001016 mSave = menu.findItem(R.id.save);
1017 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001018 return true;
1019 }
1020
1021 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001022 public boolean onPrepareOptionsMenu(Menu menu) {
1023 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001024 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001025 // Its possible there is a menu item OR a button.
1026 boolean ccFieldVisible = mCc.isShown();
1027 boolean bccFieldVisible = mBcc.isShown();
1028 if (!ccFieldVisible || !bccFieldVisible) {
1029 ccBcc.setVisible(true);
1030 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1031 : R.string.add_bcc_label));
1032 } else {
1033 ccBcc.setVisible(false);
1034 }
1035 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001036 if (mSave != null) {
1037 mSave.setEnabled(shouldSave());
1038 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001039 return true;
1040 }
1041
1042 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001043 public boolean onOptionsItemSelected(MenuItem item) {
1044 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001045 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001046 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001047 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -08001048 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001049 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001050 case R.id.add_cc_bcc:
1051 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001052 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001053 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001054 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001055 break;
1056 case R.id.send:
1057 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001058 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001059 case R.id.discard:
1060 doDiscard();
1061 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001062 case R.id.settings:
1063 Utils.showSettings(this, mAccount);
1064 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001065 case android.R.id.home:
1066 finish();
1067 break;
1068 case R.id.help_info_menu_item:
1069 // TODO: enable context sensitive help
1070 Utils.showHelp(this, mAccount.helpIntentUri, null);
1071 break;
1072 case R.id.feedback_menu_item:
1073 Utils.sendFeedback(this, mAccount);
1074 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001075 default:
1076 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001077 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001078 }
1079 return !handled ? super.onOptionsItemSelected(item) : handled;
1080 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001081
Mindy Pereira33fe9082012-01-09 16:24:30 -08001082 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001083 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001084 }
1085
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001086 private void doSave(boolean showToast, boolean resetIME) {
1087 sendOrSaveWithSanityChecks(true, showToast, false);
1088 if (resetIME) {
1089 // Clear the IME composing suggestions from the body.
1090 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1091 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001092 }
1093
Mindy Pereira82cc5662012-01-09 17:29:30 -08001094 /*package*/ interface SendOrSaveCallback {
1095 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001096 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1097 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001098 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1099 }
1100
1101 /*package*/ static class SendOrSaveTask implements Runnable {
1102 private final Context mContext;
1103 private final SendOrSaveCallback mSendOrSaveCallback;
1104 @VisibleForTesting
1105 final SendOrSaveMessage mSendOrSaveMessage;
1106
1107 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1108 SendOrSaveCallback callback) {
1109 mContext = context;
1110 mSendOrSaveCallback = callback;
1111 mSendOrSaveMessage = message;
1112 }
1113
1114 @Override
1115 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001116 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001117
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001118 final Account selectedAccount = sendOrSaveMessage.mSelectedAccount;
1119 Message message = mSendOrSaveCallback.getMessage();
1120 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001121 // If a previous draft has been saved, in an account that is different
1122 // than what the user wants to send from, remove the old draft, and treat this
1123 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001124 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001125 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1126 ContentResolver resolver = mContext.getContentResolver();
1127 ContentValues values = new ContentValues();
1128 values.put(BaseColumns._ID, messageId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001129 if (selectedAccount.expungeMessageUri != null) {
1130 resolver.update(selectedAccount.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001131 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001132 } else {
1133 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001134 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001135 // reset messageId to 0, so a new message will be created
1136 messageId = UIProvider.INVALID_MESSAGE_ID;
1137 }
1138 }
1139
1140 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001141 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001142 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001143 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001144 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1145 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001146 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001147 ContentResolver resolver = mContext.getContentResolver();
1148 Uri messageUri = resolver.insert(
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001149 sendOrSaveMessage.mSave ? selectedAccount.saveDraftUri
1150 : selectedAccount.sendMessageUri, sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001151 if (sendOrSaveMessage.mSave && messageUri != null) {
1152 Cursor messageCursor = resolver.query(messageUri,
1153 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001154 if (messageCursor != null) {
1155 try {
1156 if (messageCursor.moveToFirst()) {
1157 // Broadcast notification that a new message has
1158 // been allocated
1159 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1160 new Message(messageCursor));
1161 }
1162 } finally {
1163 messageCursor.close();
1164 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001165 }
1166 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001167 }
1168
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001169 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001170 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001171 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001172 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001173 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001174 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001175 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001176 }
1177 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1178 }
1179 }
1180
1181 // Array of the outstanding send or save tasks. Access is synchronized
1182 // with the object itself
1183 /* package for testing */
1184 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
1185 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001186 private String mSignature;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001187
1188 /*package*/ static class SendOrSaveMessage {
1189 final Account mAccount;
1190 final Account mSelectedAccount;
1191 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001192 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001193 final boolean mSave;
1194 final int mRequestId;
1195
1196 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001197 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001198 mAccount = account;
1199 mSelectedAccount = selectedAccount;
1200 mValues = values;
1201 mRefMessageId = refMessageId;
1202 mSave = save;
1203 mRequestId = mValues.hashCode() ^ hashCode();
1204 }
1205
1206 int requestId() {
1207 return mRequestId;
1208 }
1209 }
1210
1211 /**
1212 * Get the to recipients.
1213 */
1214 public String[] getToAddresses() {
1215 return getAddressesFromList(mTo);
1216 }
1217
1218 /**
1219 * Get the cc recipients.
1220 */
1221 public String[] getCcAddresses() {
1222 return getAddressesFromList(mCc);
1223 }
1224
1225 /**
1226 * Get the bcc recipients.
1227 */
1228 public String[] getBccAddresses() {
1229 return getAddressesFromList(mBcc);
1230 }
1231
1232 public String[] getAddressesFromList(RecipientEditTextView list) {
1233 if (list == null) {
1234 return new String[0];
1235 }
1236 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1237 int count = tokens.length;
1238 String[] result = new String[count];
1239 for (int i = 0; i < count; i++) {
1240 result[i] = tokens[i].toString();
1241 }
1242 return result;
1243 }
1244
1245 /**
1246 * Check for invalid email addresses.
1247 * @param to String array of email addresses to check.
1248 * @param wrongEmailsOut Emails addresses that were invalid.
1249 */
1250 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
1251 for (String email : to) {
1252 if (!mValidator.isValid(email)) {
1253 wrongEmailsOut.add(email);
1254 }
1255 }
1256 }
1257
1258 /**
1259 * Show an error because the user has entered an invalid recipient.
1260 * @param message
1261 */
1262 public void showRecipientErrorDialog(String message) {
1263 // Only 1 invalid recipients error dialog should be allowed up at a
1264 // time.
1265 if (mRecipientErrorDialog != null) {
1266 mRecipientErrorDialog.dismiss();
1267 }
1268 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1269 R.string.recipient_error_dialog_title)
1270 .setIconAttribute(android.R.attr.alertDialogIcon)
1271 .setCancelable(false)
1272 .setPositiveButton(
1273 R.string.ok, new Dialog.OnClickListener() {
1274 public void onClick(DialogInterface dialog, int which) {
1275 // after the user dismisses the recipient error
1276 // dialog we want to make sure to refocus the
1277 // recipient to field so they can fix the issue
1278 // easily
1279 if (mTo != null) {
1280 mTo.requestFocus();
1281 }
1282 mRecipientErrorDialog = null;
1283 }
1284 }).show();
1285 }
1286
1287 /**
1288 * Update the state of the UI based on whether or not the current draft
1289 * needs to be saved and the message is not empty.
1290 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001291 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001292 if (mSave != null) {
1293 mSave.setEnabled((shouldSave() && !isBlank()));
1294 }
1295 }
1296
1297 /**
1298 * Returns true if we need to save the current draft.
1299 */
1300 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001301 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001302 // The message should only be saved if:
1303 // It hasn't been sent AND
1304 // Some text has been added to the message OR
1305 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001306 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -08001307 (mReplyFromChanged && !isBlank()));
1308 }
1309 }
1310
1311 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001312 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001313 * @return boolean
1314 */
1315 public boolean isBlank() {
1316 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001317 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1318 mBodyView.getText().toString()) == 0)
1319 && mTo.length() == 0
1320 && mCc.length() == 0 && mBcc.length() == 0
1321 && mAttachmentsView.getAttachments().size() == 0;
1322 }
1323
1324 @VisibleForTesting
1325 protected int getSignatureStartPosition(String signature, String bodyText) {
1326 int startPos = -1;
1327
1328 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1329 return startPos;
1330 }
1331
1332 int bodyLength = bodyText.length();
1333 int signatureLength = signature.length();
1334 String printableVersion = convertToPrintableSignature(signature);
1335 int printableLength = printableVersion.length();
1336
1337 if (bodyLength >= printableLength
1338 && bodyText.substring(bodyLength - printableLength)
1339 .equals(printableVersion)) {
1340 startPos = bodyLength - printableLength;
1341 } else if (bodyLength >= signatureLength
1342 && bodyText.substring(bodyLength - signatureLength)
1343 .equals(signature)) {
1344 startPos = bodyLength - signatureLength;
1345 }
1346 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001347 }
1348
1349 /**
1350 * Allows any changes made by the user to be ignored. Called when the user
1351 * decides to discard a draft.
1352 */
1353 private void discardChanges() {
1354 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001355 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001356 mReplyFromChanged = false;
1357 }
1358
1359 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001360 * @param body
1361 * @param save
1362 * @param showToast
1363 * @return Whether the send or save succeeded.
1364 */
1365 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1366 final boolean orientationChanged) {
1367 String[] to, cc, bcc;
1368 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001369
Mindy Pereira181df782012-03-01 13:32:44 -08001370 if (orientationChanged) {
1371 to = cc = bcc = new String[0];
1372 } else {
1373 to = getToAddresses();
1374 cc = getCcAddresses();
1375 bcc = getBccAddresses();
1376 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001377
Mindy Pereira181df782012-03-01 13:32:44 -08001378 // Don't let the user send to nobody (but it's okay to save a message
1379 // with no recipients)
1380 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1381 showRecipientErrorDialog(getString(R.string.recipient_needed));
1382 return false;
1383 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001384
Mindy Pereira181df782012-03-01 13:32:44 -08001385 List<String> wrongEmails = new ArrayList<String>();
1386 if (!save) {
1387 checkInvalidEmails(to, wrongEmails);
1388 checkInvalidEmails(cc, wrongEmails);
1389 checkInvalidEmails(bcc, wrongEmails);
1390 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001391
Mindy Pereira181df782012-03-01 13:32:44 -08001392 // Don't let the user send an email with invalid recipients
1393 if (wrongEmails.size() > 0) {
1394 String errorText = String.format(getString(R.string.invalid_recipient),
1395 wrongEmails.get(0));
1396 showRecipientErrorDialog(errorText);
1397 return false;
1398 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001399
Mindy Pereira181df782012-03-01 13:32:44 -08001400 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
1401 public void onClick(DialogInterface dialog, int which) {
1402 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1403 }
1404 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001405
Mindy Pereira181df782012-03-01 13:32:44 -08001406 // Show a warning before sending only if there are no attachments.
1407 if (!save) {
1408 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1409 boolean warnAboutEmptySubject = isSubjectEmpty();
1410 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001411
Mindy Pereira181df782012-03-01 13:32:44 -08001412 // A warning about an empty body may not be warranted when
1413 // forwarding mails, since a common use case is to forward
1414 // quoted text and not append any more text.
1415 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001416
Mindy Pereira181df782012-03-01 13:32:44 -08001417 // When we bring up a dialog warning the user about a send,
1418 // assume that they accept sending the message. If they do not,
1419 // the dialog listener is required to enable sending again.
1420 if (warnAboutEmptySubject) {
1421 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1422 return true;
1423 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001424
Mindy Pereira181df782012-03-01 13:32:44 -08001425 if (warnAboutEmptyBody) {
1426 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1427 return true;
1428 }
1429 }
1430 // Ask for confirmation to send (if always required)
1431 if (showSendConfirmation()) {
1432 showSendConfirmDialog(R.string.confirm_send_message, listener);
1433 return true;
1434 }
1435 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001436
Mindy Pereira181df782012-03-01 13:32:44 -08001437 sendOrSave(body, save, showToast, false);
1438 return true;
1439 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001440
Mindy Pereira181df782012-03-01 13:32:44 -08001441 /**
1442 * Returns a boolean indicating whether warnings should be shown for empty
1443 * subject and body fields
1444 *
1445 * @return True if a warning should be shown for empty text fields
1446 */
1447 protected boolean showEmptyTextWarnings() {
1448 return mAttachmentsView.getAttachments().size() == 0;
1449 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001450
Mindy Pereira181df782012-03-01 13:32:44 -08001451 /**
1452 * Returns a boolean indicating whether the user should confirm each send
1453 *
1454 * @return True if a warning should be on each send
1455 */
1456 protected boolean showSendConfirmation() {
1457 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1458 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001459
Mindy Pereira181df782012-03-01 13:32:44 -08001460 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1461 if (mSendConfirmDialog != null) {
1462 mSendConfirmDialog.dismiss();
1463 mSendConfirmDialog = null;
1464 }
1465 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1466 .setTitle(R.string.confirm_send_title)
1467 .setIconAttribute(android.R.attr.alertDialogIcon)
1468 .setPositiveButton(R.string.send, listener)
1469 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1470 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001471
Mindy Pereira181df782012-03-01 13:32:44 -08001472 /**
1473 * Returns whether the ComposeArea believes there is any text in the body of
1474 * the composition. TODO: When ComposeArea controls the Body as well, add
1475 * that here.
1476 */
1477 public boolean isBodyEmpty() {
1478 return !mQuotedTextView.isTextIncluded();
1479 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001480
Mindy Pereira181df782012-03-01 13:32:44 -08001481 /**
1482 * Test to see if the subject is empty.
1483 *
1484 * @return boolean.
1485 */
1486 // TODO: this will likely go away when composeArea.focus() is implemented
1487 // after all the widget control is moved over.
1488 public boolean isSubjectEmpty() {
1489 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1490 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001491
Mindy Pereira181df782012-03-01 13:32:44 -08001492 /* package */
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001493 static int sendOrSaveInternal(Context context, final Account account,
1494 final Account selectedAccount, String fromAddress, final Spanned body,
1495 final String[] to, final String[] cc, final String[] bcc, final String subject,
1496 final CharSequence quotedText, final List<Attachment> attachments,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001497 final String refMessageId, SendOrSaveCallback callback, Handler handler, boolean save,
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001498 boolean forward) {
1499 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001500
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001501 MessageModification.putToAddresses(values, to);
1502 MessageModification.putCcAddresses(values, cc);
1503 MessageModification.putBccAddresses(values, bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001504
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001505 MessageModification.putSubject(values, subject);
1506 String htmlBody = Html.toHtml(body);
1507 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1508 StringBuilder fullBody = new StringBuilder(htmlBody);
1509 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001510 // HTML gets converted to text for now
1511 final String text = quotedText.toString();
1512 if (QuotedTextView.containsQuotedText(text)) {
1513 int pos = QuotedTextView.getQuotedTextOffset(text);
1514 fullBody.append(text.substring(0, pos));
1515 MessageModification.putIncludeQuotedText(values, true);
1516 MessageModification.putQuoteStartPos(values, pos);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001517 MessageModification.putForward(values, forward);
Mindy Pereirae8caf122012-03-20 15:23:31 -07001518 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001519 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001520 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1521 // This shouldn't happen, but just use what we have,
1522 // and don't do server-side expansion
1523 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001524 }
1525 }
1526 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1527 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereiraf944e962012-01-17 11:43:36 -08001528 MessageModification.putAttachments(values, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001529
Mindy Pereira181df782012-03-01 13:32:44 -08001530 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1531 values, refMessageId, save);
1532 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001533
Mindy Pereira181df782012-03-01 13:32:44 -08001534 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001535
Mindy Pereira181df782012-03-01 13:32:44 -08001536 // Do the send/save action on the specified handler to avoid possible
1537 // ANRs
1538 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001539
Mindy Pereira181df782012-03-01 13:32:44 -08001540 return sendOrSaveMessage.requestId();
1541 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001542
Mindy Pereira181df782012-03-01 13:32:44 -08001543 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1544 boolean orientationChanged) {
1545 // Check if user is a monkey. Monkeys can compose and hit send
1546 // button but are not allowed to send anything off the device.
1547 if (!save && ActivityManager.isUserAMonkey()) {
1548 return;
1549 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001550
Mindy Pereira181df782012-03-01 13:32:44 -08001551 String[] to, cc, bcc;
1552 if (orientationChanged) {
1553 to = cc = bcc = new String[0];
1554 } else {
1555 to = getToAddresses();
1556 cc = getCcAddresses();
1557 bcc = getBccAddresses();
1558 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001559
Mindy Pereira181df782012-03-01 13:32:44 -08001560 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001561 private int mRestoredRequestId;
1562
1563 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08001564 synchronized (mActiveTasks) {
1565 int numTasks = mActiveTasks.size();
1566 if (numTasks == 0) {
1567 // Start service so we won't be killed if this app is
1568 // put in the background.
1569 startService(new Intent(ComposeActivity.this, EmptyService.class));
1570 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001571
Mindy Pereira181df782012-03-01 13:32:44 -08001572 mActiveTasks.add(sendOrSaveTask);
1573 }
1574 if (sTestSendOrSaveCallback != null) {
1575 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1576 }
1577 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001578
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001579 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1580 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08001581 synchronized (mDraftLock) {
1582 mDraftId = message.id;
1583 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001584 if (sRequestMessageIdMap != null) {
1585 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1586 }
Mindy Pereira181df782012-03-01 13:32:44 -08001587 // Cache request message map, in case the process is killed
1588 saveRequestMap();
1589 }
1590 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001591 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08001592 }
1593 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001594
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001595 public Message getMessage() {
1596 synchronized (mDraftLock) {
1597 return mDraft;
1598 }
1599 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001600
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001601 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1602 if (success) {
1603 // Successfully sent or saved so reset change markers
1604 discardChanges();
1605 } else {
1606 // A failure happened with saving/sending the draft
1607 // TODO(pwestbro): add a better string that should be used
1608 // when failing to send or save
1609 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1610 .show();
1611 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001612
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001613 int numTasks;
1614 synchronized (mActiveTasks) {
1615 // Remove the task from the list of active tasks
1616 mActiveTasks.remove(task);
1617 numTasks = mActiveTasks.size();
1618 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001619
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001620 if (numTasks == 0) {
1621 // Stop service so we can be killed.
1622 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1623 }
1624 if (sTestSendOrSaveCallback != null) {
1625 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1626 }
1627 }
Mindy Pereira181df782012-03-01 13:32:44 -08001628 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001629
Mindy Pereira181df782012-03-01 13:32:44 -08001630 // Get the selected account if the from spinner has been setup.
1631 Account selectedAccount = mAccount;
1632 String fromAddress = selectedAccount.name;
1633 if (selectedAccount == null || fromAddress == null) {
1634 // We don't have either the selected account or from address,
1635 // use mAccount.
1636 selectedAccount = mAccount;
1637 fromAddress = mAccount.name;
1638 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001639
Mindy Pereira181df782012-03-01 13:32:44 -08001640 if (mSendSaveTaskHandler == null) {
1641 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1642 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001643
Mindy Pereira181df782012-03-01 13:32:44 -08001644 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1645 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001646
Mindy Pereira181df782012-03-01 13:32:44 -08001647 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body, to, cc,
1648 bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedText(),
1649 mAttachmentsView.getAttachments(), mRefMessageId, callback, mSendSaveTaskHandler,
1650 save, mForward);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001651
Mindy Pereira181df782012-03-01 13:32:44 -08001652 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1653 mRecipient = selectedAccount.name;
1654 }
1655 mAccount = selectedAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001656
Mindy Pereira181df782012-03-01 13:32:44 -08001657 // Don't display the toast if the user is just changing the orientation,
1658 // but we still need to save the draft to the cursor because this is how we restore
1659 // the attachments when the configuration change completes.
1660 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1661 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1662 Toast.LENGTH_LONG).show();
1663 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001664
Mindy Pereira181df782012-03-01 13:32:44 -08001665 // Need to update variables here because the send or save completes
1666 // asynchronously even though the toast shows right away.
1667 discardChanges();
1668 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001669
Mindy Pereira181df782012-03-01 13:32:44 -08001670 // If we are sending, finish the activity
1671 if (!save) {
1672 finish();
1673 }
1674 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001675
Mindy Pereira181df782012-03-01 13:32:44 -08001676 /**
1677 * Save the state of the request messageid map. This allows for the Gmail
1678 * process to be killed, but and still allow for ComposeActivity instances
1679 * to be recreated correctly.
1680 */
1681 private void saveRequestMap() {
1682 // TODO: store the request map in user preferences.
1683 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001684
Mindy Pereira013194c2012-01-06 15:09:33 -08001685 public void doAttach() {
1686 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1687 i.addCategory(Intent.CATEGORY_OPENABLE);
Mindy Pereira181df782012-03-01 13:32:44 -08001688 if (android.provider.Settings.System.getInt(getContentResolver(),
1689 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08001690 i.setType("*/*");
1691 } else {
1692 i.setType("image/*");
1693 }
1694 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08001695 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
1696 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08001697 }
1698
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001699 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001700 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001701 if (mCcBccButton != null) {
1702 mCcBccButton.setVisibility(View.GONE);
1703 }
1704 }
1705
Mindy Pereira326c6602012-01-04 15:32:42 -08001706 @Override
1707 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001708 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001709 if (position == ComposeActivity.REPLY) {
1710 mComposeMode = ComposeActivity.REPLY;
1711 } else if (position == ComposeActivity.REPLY_ALL) {
1712 mComposeMode = ComposeActivity.REPLY_ALL;
1713 } else if (position == ComposeActivity.FORWARD) {
1714 mComposeMode = ComposeActivity.FORWARD;
1715 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001716 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001717 resetMessageForModeChange();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001718 if (mRefMessage != null) {
1719 initFromRefMessage(mComposeMode, mAccount.name);
1720 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001721 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001722 return true;
1723 }
1724
Mindy Pereira154386a2012-01-11 13:02:33 -08001725 private void resetMessageForModeChange() {
1726 // When switching between reply, reply all, forward,
1727 // follow the behavior of webview.
1728 // The contents of the following fields are cleared
1729 // so that they can be populated directly from the
1730 // ref message:
1731 // 1) Any recipient fields
1732 // 2) The subject
1733 mTo.setText("");
1734 mCc.setText("");
1735 mBcc.setText("");
1736 // Any edits to the subject are replaced with the original subject.
1737 mSubject.setText("");
1738
1739 // Any changes to the contents of the following fields are kept:
1740 // 1) Body
1741 // 2) Attachments
1742 // If the user made changes to attachments, keep their changes.
1743 if (!mAttachmentsChanged) {
1744 mAttachmentsView.deleteAllAttachments();
1745 }
1746 }
1747
Mindy Pereira326c6602012-01-04 15:32:42 -08001748 private class ComposeModeAdapter extends ArrayAdapter<String> {
1749
1750 private LayoutInflater mInflater;
1751
1752 public ComposeModeAdapter(Context context) {
1753 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1754 .getStringArray(R.array.compose_modes));
1755 }
1756
1757 private LayoutInflater getInflater() {
1758 if (mInflater == null) {
1759 mInflater = LayoutInflater.from(getContext());
1760 }
1761 return mInflater;
1762 }
1763
1764 @Override
1765 public View getView(int position, View convertView, ViewGroup parent) {
1766 if (convertView == null) {
1767 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1768 }
1769 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1770 return super.getView(position, convertView, parent);
1771 }
1772 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001773
1774 @Override
1775 public void onRespondInline(String text) {
1776 appendToBody(text, false);
1777 }
1778
1779 /**
1780 * Append text to the body of the message. If there is no existing body
1781 * text, just sets the body to text.
1782 *
1783 * @param text
1784 * @param withSignature True to append a signature.
1785 */
1786 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001787 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001788 if (bodyText != null && bodyText.length() > 0) {
1789 bodyText.append(text);
1790 } else {
1791 setBody(text, withSignature);
1792 }
1793 }
1794
1795 /**
1796 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001797 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001798 * @param text
1799 * @param withSignature True to append a signature.
1800 */
1801 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001802 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001803 if (withSignature) {
1804 appendSignature();
1805 }
1806 }
1807
1808 private void appendSignature() {
1809 mSignature = mCachedSettings != null ? mCachedSettings.signature : null;
1810 if (!TextUtils.isEmpty(mSignature)) {
1811 // Appending a signature does not count as changing text.
1812 mBodyView.removeTextChangedListener(this);
1813 mBodyView.append(convertToPrintableSignature(mSignature));
1814 mBodyView.addTextChangedListener(this);
1815 }
1816 }
1817
1818 private String convertToPrintableSignature(String signature) {
1819 String signatureResource = getResources().getString(R.string.signature);
1820 if (signature == null) {
1821 signature = "";
1822 }
1823 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001824 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001825
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001826 @Override
1827 public void onAccountChanged() {
1828 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereira181df782012-03-01 13:32:44 -08001829 if (!mAccount.equals(selectedAccountInfo)) {
1830 mAccount = selectedAccountInfo;
1831 mCachedSettings = null;
1832 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
1833 // TODO: handle discarding attachments when switching accounts.
1834 // Only enable save for this draft if there is any other content
1835 // in the message.
1836 if (!isBlank()) {
1837 enableSave(true);
1838 }
1839 mReplyFromChanged = true;
1840 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001841 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001842 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001843
1844 public void enableSave(boolean enabled) {
1845 if (mSave != null) {
1846 mSave.setEnabled(enabled);
1847 }
1848 }
1849
1850 public void enableSend(boolean enabled) {
1851 if (mSend != null) {
1852 mSend.setEnabled(enabled);
1853 }
1854 }
1855
1856 /**
1857 * Handles button clicks from any error dialogs dealing with sending
1858 * a message.
1859 */
1860 @Override
1861 public void onClick(DialogInterface dialog, int which) {
1862 switch (which) {
1863 case DialogInterface.BUTTON_POSITIVE: {
1864 doDiscardWithoutConfirmation(true /* show toast */ );
1865 break;
1866 }
1867 case DialogInterface.BUTTON_NEGATIVE: {
1868 // If the user cancels the send, re-enable the send button.
1869 enableSend(true);
1870 break;
1871 }
1872 }
1873
1874 }
1875
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001876 private void doDiscard() {
1877 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
1878 .setPositiveButton(R.string.ok, this)
1879 .setNegativeButton(R.string.cancel, null)
1880 .create().show();
1881 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001882 /**
1883 * Effectively discard the current message.
1884 *
1885 * This method is either invoked from the menu or from the dialog
1886 * once the user has confirmed that they want to discard the message.
1887 * @param showToast show "Message discarded" toast if true
1888 */
1889 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001890 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001891 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1892 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07001893 values.put(BaseColumns._ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001894 if (mAccount.expungeMessageUri != null) {
1895 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
1896 } else {
1897 // TODO(mindyp): call delete on this conversation instead.
1898 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001899 // This is not strictly necessary (since we should not try to
1900 // save the draft after calling this) but it ensures that if we
1901 // do save again for some reason we make a new draft rather than
1902 // trying to resave an expunged draft.
1903 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1904 }
1905 }
1906
1907 if (showToast) {
1908 // Display a toast to let the user know
1909 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1910 }
1911
1912 // This prevents the draft from being saved in onPause().
1913 discardChanges();
1914 finish();
1915 }
1916
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001917 private void saveIfNeeded() {
1918 if (mAccount == null) {
1919 // We have not chosen an account yet so there's no way that we can save. This is ok,
1920 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1921 // user has not interacted with us yet and there is no real state to save.
1922 return;
1923 }
1924
1925 if (shouldSave()) {
1926 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1927 }
1928 }
1929
1930 private void saveIfNeededOnOrientationChanged() {
1931 if (mAccount == null) {
1932 // We have not chosen an account yet so there's no way that we can save. This is ok,
1933 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1934 // user has not interacted with us yet and there is no real state to save.
1935 return;
1936 }
1937
1938 if (shouldSave()) {
1939 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1940 }
1941 }
1942
1943 /**
1944 * Save a draft if a draft already exists or the message is not empty.
1945 */
1946 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1947 saveOnOrientationChanged();
1948 if (resetIME) {
1949 // Clear the IME composing suggestions from the body.
1950 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1951 }
1952 }
1953
1954 protected boolean saveOnOrientationChanged() {
1955 return sendOrSaveWithSanityChecks(true, false, true);
1956 }
1957
1958 @Override
1959 public void onAttachmentDeleted() {
1960 mAttachmentsChanged = true;
1961 updateSaveUi();
1962 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001963
1964
1965 /**
1966 * This is called any time one of our text fields changes.
1967 */
1968 public void afterTextChanged(Editable s) {
1969 mTextChanged = true;
1970 updateSaveUi();
1971 }
1972
1973 @Override
1974 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1975 // Do nothing.
1976 }
1977
1978 public void onTextChanged(CharSequence s, int start, int before, int count) {
1979 // Do nothing.
1980 }
1981
1982
1983 // There is a big difference between the text associated with an address changing
1984 // to add the display name or to format properly and a recipient being added or deleted.
1985 // Make sure we only notify of changes when a recipient has been added or deleted.
1986 private class RecipientTextWatcher implements TextWatcher {
1987 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
1988
1989 private RecipientEditTextView mView;
1990
1991 private TextWatcher mListener;
1992
1993 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
1994 mView = view;
1995 mListener = listener;
1996 }
1997
1998 @Override
1999 public void afterTextChanged(Editable s) {
2000 if (hasChanged()) {
2001 mListener.afterTextChanged(s);
2002 }
2003 }
2004
2005 private boolean hasChanged() {
2006 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2007 int totalCount = currRecips.length;
2008 int totalPrevCount = 0;
2009 for (Entry<String, Integer> entry : mContent.entrySet()) {
2010 totalPrevCount += entry.getValue();
2011 }
2012 if (totalCount != totalPrevCount) {
2013 return true;
2014 }
2015
2016 for (String recip : currRecips) {
2017 if (!mContent.containsKey(recip)) {
2018 return true;
2019 } else {
2020 int count = mContent.get(recip) - 1;
2021 if (count < 0) {
2022 return true;
2023 } else {
2024 mContent.put(recip, count);
2025 }
2026 }
2027 }
2028 return false;
2029 }
2030
2031 private String[] tokenizeRecips(String[] recips) {
2032 // Tokenize them all and put them in the list.
2033 String[] recipAddresses = new String[recips.length];
2034 for (int i = 0; i < recips.length; i++) {
2035 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2036 }
2037 return recipAddresses;
2038 }
2039
2040 @Override
2041 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2042 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2043 for (String recip : recips) {
2044 if (!mContent.containsKey(recip)) {
2045 mContent.put(recip, 1);
2046 } else {
2047 mContent.put(recip, (mContent.get(recip)) + 1);
2048 }
2049 }
2050 }
2051
2052 @Override
2053 public void onTextChanged(CharSequence s, int start, int before, int count) {
2054 // Do nothing.
2055 }
2056 }
Mindy Pereira181df782012-03-01 13:32:44 -08002057
2058 @Override
2059 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
2060 if (id == ACCOUNT_SETTINGS_LOADER) {
Mindy Pereira23e9fde2012-03-20 15:08:24 -07002061 if (mAccount != null && mAccount.settingsQueryUri != null) {
Mindy Pereira181df782012-03-01 13:32:44 -08002062 return new CursorLoader(this, mAccount.settingsQueryUri,
2063 UIProvider.SETTINGS_PROJECTION, null, null, null);
2064 }
2065 }
2066 return null;
2067 }
2068
2069 @Override
2070 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
2071 if (loader.getId() == ACCOUNT_SETTINGS_LOADER) {
2072 if (data != null) {
2073 data.moveToFirst();
2074 mCachedSettings = new Settings(data);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002075 appendSignature();
Mindy Pereira181df782012-03-01 13:32:44 -08002076 }
2077 }
2078 }
2079
2080 @Override
2081 public void onLoaderReset(Loader<Cursor> loader) {
2082 // Do nothing.
2083 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002084}