blob: ba7f7aad1d6fc55df3a6c6f953f4fe3662c2f04c [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;
Mindy Pereira12575862012-03-21 16:30:54 -070075import com.android.mail.providers.UIProvider.DraftType;
Andy Huang30e2c242012-01-06 18:14:30 -080076import com.android.mail.R;
Paul Westbrook92227f62012-03-20 10:32:51 -070077import com.android.mail.utils.AccountUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080078import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080079import com.android.mail.utils.Utils;
Mindy Pereirac17d0732011-12-29 10:46:19 -080080import com.android.ex.chips.RecipientEditTextView;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080081import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080082import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080083import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080084
Mindy Pereira8eca57a2012-03-20 16:42:34 -070085import java.io.UnsupportedEncodingException;
86import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080087import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070088import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080089import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080090import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080091import java.util.HashSet;
92import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -070093import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070094import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -080095import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080096
97public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080098 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Mindy Pereira181df782012-03-01 13:32:44 -080099 AttachmentDeletedListener, OnAccountChangedListener, LoaderCallbacks<Cursor> {
Mindy Pereira6349a042012-01-04 11:25:01 -0800100 // Identifiers for which type of composition this is
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700101 static final int COMPOSE = -1;
Mindy Pereira6349a042012-01-04 11:25:01 -0800102 static final int REPLY = 0;
103 static final int REPLY_ALL = 1;
104 static final int FORWARD = 2;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700105 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800106
107 // Integer extra holding one of the above compose action
108 private static final String EXTRA_ACTION = "action";
109
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700110 private static final String UTF8_ENCODING_NAME = "UTF-8";
111
112 private static final String MAIL_TO = "mailto";
113
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700114 private static final String EXTRA_SUBJECT = "subject";
115
116 private static final String EXTRA_BODY = "body";
117
118 // Extra that we can get passed from other activities
119 private static final String EXTRA_TO = "to";
120 private static final String EXTRA_CC = "cc";
121 private static final String EXTRA_BCC = "bcc";
122
123 // List of all the fields
124 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
125
Mindy Pereira82cc5662012-01-09 17:29:30 -0800126 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
127 // Map containing information about requests to create new messages, and the id of the
128 // messages that were the result of those requests.
129 //
130 // This map is used when the activity that initiated the save a of a new message, is killed
131 // before the save has completed (and when we know the id of the newly created message). When
132 // a save is completed, the service that is running in the background, will update the map
133 //
134 // When a new ComposeActivity instance is created, it will attempt to use the information in
135 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
136 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
137 // to populate the map with data stored in shared preferences.
138 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
139 // Key used to store the above map
140 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800141 /**
142 * Notifies the {@code Activity} that the caller is an Email
143 * {@code Activity}, so that the back behavior may be modified accordingly.
144 *
145 * @see #onAppUpPressed
146 */
147 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
148
Paul Westbrookf97588b2012-03-20 11:11:37 -0700149 static final String EXTRA_ATTACHMENTS = "attachments";
150
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800151 // If this is a reply/forward then this extra will hold the original message
152 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700153 // If this is an action to edit an existing draft messagge, this extra will hold the
154 // draft message
155 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800156 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800157 private static final String LOG_TAG = new LogUtils().getLogTag();
158 // Request numbers for activities we start
159 private static final int RESULT_PICK_ATTACHMENT = 1;
160 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira181df782012-03-01 13:32:44 -0800161 private static final int ACCOUNT_SETTINGS_LOADER = 0;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700162 // TODO(mindyp) set mime-type for auto send?
163 private static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
164
165 // Max size for attachments (5 megs). Will be overridden by account settings if found.
166 // TODO(mindyp): read this from account settings?
167 private static final int DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800168
Mindy Pereira82cc5662012-01-09 17:29:30 -0800169 /**
170 * A single thread for running tasks in the background.
171 */
172 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800173 private RecipientEditTextView mTo;
174 private RecipientEditTextView mCc;
175 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800176 private Button mCcBccButton;
177 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800178 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800179 private Account mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800180 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800181 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800182 private TextView mSubject;
183
Mindy Pereira326c6602012-01-04 15:32:42 -0800184 private ComposeModeAdapter mComposeModeAdapter;
185 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800186 private boolean mForward;
187 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800188 private QuotedTextView mQuotedTextView;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800189 private TextView mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800190 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800191 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800192 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800193 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800194 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800195 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800196 private boolean mTextChanged;
197 private boolean mReplyFromChanged;
198 private MenuItem mSave;
199 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800200 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800201 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800202 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800203 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
204 private Message mDraft;
205 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800206 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800207
Mindy Pereira326c6602012-01-04 15:32:42 -0800208 /**
209 * Can be called from a non-UI thread.
210 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800211 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700212 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800213 }
214
Mindy Pereira6349a042012-01-04 11:25:01 -0800215 /**
216 * Can be called from a non-UI thread.
217 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800218 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800219 launch(launcher, account, null, COMPOSE);
220 }
221
222 /**
223 * Can be called from a non-UI thread.
224 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800225 public static void reply(Context launcher, Account account, Message message) {
226 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800227 }
228
229 /**
230 * Can be called from a non-UI thread.
231 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800232 public static void replyAll(Context launcher, Account account, Message message) {
233 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800234 }
235
236 /**
237 * Can be called from a non-UI thread.
238 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800239 public static void forward(Context launcher, Account account, Message message) {
240 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800241 }
242
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800243 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800244 Intent intent = new Intent(launcher, ComposeActivity.class);
245 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
246 intent.putExtra(EXTRA_ACTION, action);
247 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700248 if (action == EDIT_DRAFT) {
249 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
250 } else {
251 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
252 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800253 launcher.startActivity(intent);
254 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800255
256 @Override
257 public void onCreate(Bundle savedInstanceState) {
258 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800259 setContentView(R.layout.compose);
260 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800261 Intent intent = getIntent();
Paul Westbrook92227f62012-03-20 10:32:51 -0700262
263 Account account = (Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
264 if (account == null) {
265 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
266 if (syncingAccounts.length > 0) {
267 account = syncingAccounts[0];
268 }
269 }
270
271 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800272 if (mAccount == null) {
273 return;
274 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800275 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800276 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereira29ef1b82012-01-13 11:26:21 -0800277 if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700278 if (mRefMessage != null) {
279 initFromRefMessage(action, mAccount.name);
280 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700281 } else if (action == EDIT_DRAFT) {
282 // Initialize the message from the message in the intent
283 final Message message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
284
285 initFromMessage(message);
286
287 // Update the action to the draft type of the previous draft
288 switch (message.draftType) {
289 case UIProvider.DraftType.REPLY:
290 action = REPLY;
291 break;
292 case UIProvider.DraftType.REPLY_ALL:
293 action = REPLY_ALL;
294 break;
295 case UIProvider.DraftType.FORWARD:
296 action = FORWARD;
297 break;
298 case UIProvider.DraftType.COMPOSE:
299 default:
300 action = COMPOSE;
301 break;
302 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700303 } else {
304 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700305 }
306
307 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800308 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800309 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800310 initRecipients();
Paul Westbrookf97588b2012-03-20 11:11:37 -0700311 initAttachmentsFromIntent(intent);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800312 initActionBar(action);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700313 initFromSpinner(action);
Mindy Pereira75f66632012-01-11 11:42:02 -0800314 initChangeListeners();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800315 }
316
317 @Override
318 protected void onResume() {
319 super.onResume();
320 // Update the from spinner as other accounts
321 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800322 if (mFromSpinner != null && mAccount != null) {
323 mFromSpinner.asyncInitFromSpinner();
324 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800325 }
326
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800327 @Override
328 protected void onPause() {
329 super.onPause();
330
331 if (mSendConfirmDialog != null) {
332 mSendConfirmDialog.dismiss();
333 }
334 if (mRecipientErrorDialog != null) {
335 mRecipientErrorDialog.dismiss();
336 }
337
338 saveIfNeeded();
339 }
340
341 @Override
342 protected final void onActivityResult(int request, int result, Intent data) {
343 mAddingAttachment = false;
344
345 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
346 addAttachmentAndUpdateView(data);
347 }
348 }
349
350 @Override
351 public final void onSaveInstanceState(Bundle state) {
352 super.onSaveInstanceState(state);
353
354 // onSaveInstanceState is only called if the user might come back to this activity so it is
355 // not an ideal location to save the draft. However, if we have never saved the draft before
356 // we have to save it here in order to have an id to save in the bundle.
357 saveIfNeededOnOrientationChanged();
358 }
359
Mindy Pereira818143e2012-01-11 13:59:49 -0800360 @VisibleForTesting
361 void setAccount(Account account) {
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700362 assert(account != null);
363 if (!account.equals(mAccount)) {
364 mAccount = account;
365 }
Mindy Pereira181df782012-03-01 13:32:44 -0800366 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800367 }
368
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700369 private void initFromSpinner(int action) {
370 if (action == COMPOSE ||
371 (action == EDIT_DRAFT
372 && mDraft.draftType == UIProvider.DraftType.COMPOSE)) {
373 mFromSpinner.setCurrentAccount(mAccount);
374 mFromSpinner.asyncInitFromSpinner();
375 boolean showSpinner = mFromSpinner.getCount() > 1;
376 // If there is only 1 account, just show that account.
377 // Otherwise, give the user the ability to choose which account to
378 // send
379 // mail from / save drafts to.
380 mFromStatic.setVisibility(showSpinner ? View.GONE : View.VISIBLE);
381 mFromStaticText.setText(mAccount.name);
382 mFromSpinnerWrapper.setVisibility(showSpinner ? View.VISIBLE : View.GONE);
383 } else {
384 mFromStatic.setVisibility(View.VISIBLE);
385 mFromStaticText.setText(mAccount.name);
386 mFromSpinnerWrapper.setVisibility(View.GONE);
387 mFromSpinner.setCurrentAccount(mAccount);
388 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800389 }
390
391 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800392 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800393 if (mCcBccButton != null) {
394 mCcBccButton.setOnClickListener(this);
395 }
396 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800397 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800398 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
399 if (mAttachmentsButton != null) {
400 mAttachmentsButton.setOnClickListener(this);
401 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800402 mTo = (RecipientEditTextView) findViewById(R.id.to);
403 mCc = (RecipientEditTextView) findViewById(R.id.cc);
404 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800405 // TODO: add special chips text change watchers before adding
406 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800407 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800408 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
409 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800410 mBodyView = (TextView) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800411 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800412 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800413 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800414 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800415 }
416
Mindy Pereira75f66632012-01-11 11:42:02 -0800417 // Now that the message has been initialized from any existing draft or
418 // ref message data, set up listeners for any changes that occur to the
419 // message.
420 private void initChangeListeners() {
421 mSubject.addTextChangedListener(this);
422 mBodyView.addTextChangedListener(this);
423 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
424 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
425 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
426 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800427 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800428 }
429
Mindy Pereira326c6602012-01-04 15:32:42 -0800430 private void initActionBar(int action) {
431 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800432 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800433 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800434 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
435 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800436 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800437 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800438 if (mComposeModeAdapter == null) {
439 mComposeModeAdapter = new ComposeModeAdapter(this);
440 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800441 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
442 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800443 switch (action) {
444 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800445 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800446 break;
447 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800448 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800449 break;
450 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800451 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800452 break;
453 }
454 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700455 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
456 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
457 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800458 }
459
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800460 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700461 setSubject(mRefMessage, action);
462 // Setup recipients
463 if (action == FORWARD) {
464 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800465 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700466 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
467 initBodyFromRefMessage(mRefMessage, action);
468 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
469 initAttachments(mRefMessage);
470 }
471 updateHideOrShowCcBcc();
Mindy Pereirac17d0732011-12-29 10:46:19 -0800472 }
473
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700474 private void initFromMessage(Message message) {
475 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
476
477 mDraft = message;
478 mDraftId = message.id;
479 mSubject.setText(message.subject);
480 mForward = message.draftType == UIProvider.DraftType.FORWARD;
481 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
482 addToAddresses(toAddresses);
483 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
484 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -0700485 if (message.hasAttachments) {
486 List<Attachment> attachments = message.getAttachments();
487 for (Attachment a : attachments) {
488 addAttachmentAndUpdateView(a.uri);
489 }
490 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700491
492 // Set the body
493 if (!TextUtils.isEmpty(message.bodyHtml)) {
494 mBodyView.setText(Html.fromHtml(message.bodyHtml));
495 } else {
496 mBodyView.setText(message.bodyText);
497 }
498
499 // TODO: load attachments from the previous message
500 // TODO: set the from address spinner to the right from account
501 // TODO: initialize quoted text value
502 }
503
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700504 /**
505 * Fill all the widgets with the content found in the Intent Extra, if any.
506 * Also apply the same style to all widgets. Note: if initFromExtras is
507 * called as a result of switching between reply, reply all, and forward per
508 * the latest revision of Gmail, and the user has already made changes to
509 * attachments on a previous incarnation of the message (as a reply, reply
510 * all, or forward), the original attachments from the message will not be
511 * re-instantiated. The user's changes will be respected. This follows the
512 * web gmail interaction.
513 */
514 public void initFromExtras(Intent intent) {
515
516 // If we were invoked with a SENDTO intent, the value
517 // should take precedence
518 final Uri dataUri = intent.getData();
519 if (dataUri != null) {
520 if (MAIL_TO.equals(dataUri.getScheme())) {
521 initFromMailTo(dataUri.toString());
522 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -0700523 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700524 String toText = dataUri.getSchemeSpecificPart();
525 if (toText != null) {
526 mTo.setText("");
527 addToAddresses(Arrays.asList(toText.split(",")));
528 }
529 }
530 }
531 }
532
533 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
534 if (extraStrings != null) {
535 addToAddresses(Arrays.asList(extraStrings));
536 }
537 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
538 if (extraStrings != null) {
539 addCcAddresses(Arrays.asList(extraStrings), null);
540 }
541 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
542 if (extraStrings != null) {
543 addBccAddresses(Arrays.asList(extraStrings));
544 }
545
546 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
547 if (extraString != null) {
548 mSubject.setText(extraString);
549 }
550
551 for (String extra : ALL_EXTRAS) {
552 if (intent.hasExtra(extra)) {
553 String value = intent.getStringExtra(extra);
554 if (EXTRA_TO.equals(extra)) {
555 addToAddresses(Arrays.asList(value.split(",")));
556 } else if (EXTRA_CC.equals(extra)) {
557 addCcAddresses(Arrays.asList(value.split(",")), null);
558 } else if (EXTRA_BCC.equals(extra)) {
559 addBccAddresses(Arrays.asList(value.split(",")));
560 } else if (EXTRA_SUBJECT.equals(extra)) {
561 mSubject.setText(value);
562 } else if (EXTRA_BODY.equals(extra)) {
563 setBody(value, true /* with signature */);
564 }
565 }
566 }
567
568 Bundle extras = intent.getExtras();
569 if (extras != null) {
570 final String action = intent.getAction();
571 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
572 if (text != null) {
573 setBody(text, true /* with signature */);
574 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700575 }
576
577 updateHideOrShowCcBcc();
578 }
579
580 @VisibleForTesting
581 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
582 // TODO: handle the case where there are spaces in the display name as well as the email
583 // such as "Guy with spaces <guy+with+spaces@gmail.com>" as they it could be encoded
584 // ambiguously.
585
586 // Since URLDecode.decode changes + into ' ', and + is a valid
587 // email character, we need to find/ replace these ourselves before
588 // decoding.
589 String replacePlus = s.replace("+", "%2B");
590 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
591 }
592
593 /**
594 * Initialize the compose view from a String representing a mailTo uri.
595 * @param mailToString The uri as a string.
596 */
597 public void initFromMailTo(String mailToString) {
598 // We need to disguise this string as a URI in order to parse it
599 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
600 Uri uri = Uri.parse("foo://" + mailToString);
601 int index = mailToString.indexOf("?");
602 int length = "mailto".length() + 1;
603 String to;
604 try {
605 // Extract the recipient after mailto:
606 if (index == -1) {
607 to = decodeEmailInUri(mailToString.substring(length));
608 } else {
609 to = decodeEmailInUri(mailToString.substring(length, index));
610 }
611 addToAddresses(Arrays.asList(to.split(" ,")));
612 } catch (UnsupportedEncodingException e) {
613 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
614 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
615 } else {
616 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
617 }
618 }
619
620 List<String> cc = uri.getQueryParameters("cc");
621 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
622
623 List<String> otherTo = uri.getQueryParameters("to");
624 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
625
626 List<String> bcc = uri.getQueryParameters("bcc");
627 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
628
629 List<String> subject = uri.getQueryParameters("subject");
630 if (subject.size() > 0) {
631 try {
632 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
633 } catch (UnsupportedEncodingException e) {
634 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
635 e.getMessage(), subject);
636 }
637 }
638
639 List<String> body = uri.getQueryParameters("body");
640 if (body.size() > 0) {
641 try {
642 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
643 true /* with signature */);
644 } catch (UnsupportedEncodingException e) {
645 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
646 }
647 }
648
649 updateHideOrShowCcBcc();
650 }
651
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800652 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800653 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800654 }
655
Paul Westbrookf97588b2012-03-20 11:11:37 -0700656 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -0700657 Bundle extras = intent.getExtras();
658 if (extras == null) {
659 extras = Bundle.EMPTY;
660 }
Paul Westbrookf97588b2012-03-20 11:11:37 -0700661 final String action = intent.getAction();
662 if (!mAttachmentsChanged) {
663 long totalSize = 0;
664 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
665 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
666 for (String uriString : uris) {
667 final Uri uri = Uri.parse(uriString);
668 long size = 0;
669 try {
670 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
671 true /* local file */);
672 } catch (AttachmentFailureException e) {
673 // A toast has already been shown to the user,
674 // just break out of the loop.
675 LogUtils.e(LOG_TAG, e, "Error adding attachment");
676 }
677 totalSize += size;
678 }
679 }
680 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
681 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
682 long size = 0;
683 try {
684 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
685 true /* local file */);
686 } catch (AttachmentFailureException e) {
687 // A toast has already been shown to the user, so just
688 // exit.
689 LogUtils.e(LOG_TAG, e, "Error adding attachment");
690 }
691 totalSize += size;
692 }
693
694 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
695 && extras.containsKey(Intent.EXTRA_STREAM)) {
696 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
697 for (Parcelable uri : uris) {
698 long size = 0;
699 try {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700700 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri,
Paul Westbrookf97588b2012-03-20 11:11:37 -0700701 false /* doSave */, true /* local file */);
702 } catch (AttachmentFailureException e) {
703 // A toast has already been shown to the user,
704 // just break out of the loop.
705 LogUtils.e(LOG_TAG, e, "Error adding attachment");
706 }
707 totalSize += size;
708 }
709 }
710
711 if (totalSize > 0) {
712 mAttachmentsChanged = true;
713 updateSaveUi();
714 }
715 }
716 }
717
718
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800719 private void initBodyFromRefMessage(Message refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800720 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800721 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
722 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800723 }
724
725 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800726 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800727 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
728 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
729 if (ccVisible || bccVisible) {
730 mCcBccView.show(false, ccVisible, bccVisible);
731 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800732 if (mCcBccButton != null) {
733 if (!mCc.isShown() || !mBcc.isShown()) {
734 mCcBccButton.setVisibility(View.VISIBLE);
735 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
736 : R.string.add_bcc_label));
737 } else {
738 mCcBccButton.setVisibility(View.GONE);
739 }
740 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800741 }
742
Mindy Pereira013194c2012-01-06 15:09:33 -0800743 /**
744 * Add attachment and update the compose area appropriately.
745 * @param data
746 */
747 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -0700748 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
749 }
750
751 public void addAttachmentAndUpdateView(Uri uri) {
752 if (uri == null) {
753 return;
754 }
Mindy Pereira013194c2012-01-06 15:09:33 -0800755 try {
Mindy Pereira2421dc82012-03-27 13:32:31 -0700756 long size = mAttachmentsView.addAttachment(mAccount, uri,
757 false /* doSave */,
Mindy Pereiraf944e962012-01-17 11:43:36 -0800758 true /* local file */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800759 if (size > 0) {
760 mAttachmentsChanged = true;
761 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800762 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800763 } catch (AttachmentFailureException e) {
764 // A toast has already been shown to the user, no need to do
765 // anything.
766 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800767 }
768 }
769
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800770 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800771 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800772 // Don't populate the address if this is a forward.
773 if (action == ComposeActivity.FORWARD) {
774 return;
775 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800776 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800777 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800778
Mindy Pereira818143e2012-01-11 13:59:49 -0800779 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800780 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800781 // This is the email address of the current user, i.e. the one composing
782 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800783 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800784 String fromAddress = refMessage.from;
785 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
786 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800787 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800788
789 // If this is a reply, the Cc list is empty. If this is a reply-all, the
790 // Cc list is the union of the To and Cc recipients of the original
791 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800792 // already on the To list.
793 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800794 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
795 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800796 addToAddresses(toAddresses);
797 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800798 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800799 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
800 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800801 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800802 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800803 addRecipients(accountEmail, ccAddresses,
804 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800805 addCcAddresses(ccAddresses, toAddresses);
806 }
807 }
808
809 private void addToAddresses(Collection<String> addresses) {
810 addAddressesToList(addresses, mTo);
811 }
812
813 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700814 addCcAddressesToList(tokenizeAddressList(addresses),
815 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800816 }
817
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700818 private void addBccAddresses(Collection<String> addresses) {
819 addAddressesToList(addresses, mBcc);
820 }
821
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800822 @VisibleForTesting
823 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
824 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
825 String address;
826
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700827 if (compareToList == null) {
828 for (Rfc822Token[] tokens : addresses) {
829 for (int i = 0; i < tokens.length; i++) {
830 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800831 list.append(address + END_TOKEN);
832 }
833 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700834 } else {
835 HashSet<String> compareTo = convertToHashSet(compareToList);
836 for (Rfc822Token[] tokens : addresses) {
837 for (int i = 0; i < tokens.length; i++) {
838 address = tokens[i].toString();
839 // Check if this is a duplicate:
840 if (!compareTo.contains(tokens[i].getAddress())) {
841 // Get the address here
842 list.append(address + END_TOKEN);
843 }
844 }
845 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800846 }
847 }
848
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800849 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
850 HashSet<String> hash = new HashSet<String>();
851 for (Rfc822Token[] tokens : list) {
852 for (int i = 0; i < tokens.length; i++) {
853 hash.add(tokens[i].getAddress());
854 }
855 }
856 return hash;
857 }
858
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800859 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
860 @VisibleForTesting
861 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
862
863 for (String address: addresses) {
864 tokenized.add(Rfc822Tokenizer.tokenize(address));
865 }
866 return tokenized;
867 }
868
869 @VisibleForTesting
870 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
871 for (String address : addresses) {
872 addAddressToList(address, list);
873 }
874 }
875
876 private void addAddressToList(String address, RecipientEditTextView list) {
877 if (address == null || list == null)
878 return;
879
880 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
881
882 for (int i = 0; i < tokens.length; i++) {
883 list.append(tokens[i] + END_TOKEN);
884 }
885 }
886
887 @VisibleForTesting
888 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800889 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800890 // The To recipient is the reply-to address specified in the original
891 // message, unless it is:
892 // the current user OR a custom from of the current user, in which case
893 // it's the To recipient list of the original message.
894 // OR missing, in which case use the sender of the original message
895 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800896 if (!TextUtils.isEmpty(replyToAddress)) {
897 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800898 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800899 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800900 }
901 return toAddresses;
902 }
903
904 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
905 for (String email : addresses) {
906 // Do not add this account, or any of the custom froms, to the list
907 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800908 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800909 if (!account.equalsIgnoreCase(recipientAddress)) {
910 recipients.add(email.replace("\"\"", ""));
911 }
912 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800913 }
914
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800915 private void setSubject(Message refMessage, int action) {
916 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800917 String prefix;
918 String correctedSubject = null;
919 if (action == ComposeActivity.COMPOSE) {
920 prefix = "";
921 } else if (action == ComposeActivity.FORWARD) {
922 prefix = getString(R.string.forward_subject_label);
923 } else {
924 prefix = getString(R.string.reply_subject_label);
925 }
926
927 // Don't duplicate the prefix
928 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
929 correctedSubject = subject;
930 } else {
931 correctedSubject = String
932 .format(getString(R.string.formatted_subject), prefix, subject);
933 }
934 mSubject.setText(correctedSubject);
935 }
936
Mindy Pereira818143e2012-01-11 13:59:49 -0800937 private void initRecipients() {
938 setupRecipients(mTo);
939 setupRecipients(mCc);
940 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800941 }
942
Mindy Pereira818143e2012-01-11 13:59:49 -0800943 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800944 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800945 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800946 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800947 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800948 int offset = accountName.indexOf("@") + 1;
949 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800950 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800951 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800952 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800953 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800954 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800955 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800956 }
957
958 @Override
959 public void onClick(View v) {
960 int id = v.getId();
961 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800962 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800963 // Verify that cc/ bcc aren't showing.
964 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800965 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800966 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800967 case R.id.add_attachment:
968 doAttach();
969 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800970 }
971 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800972
973 @Override
974 public boolean onCreateOptionsMenu(Menu menu) {
975 super.onCreateOptionsMenu(menu);
976 MenuInflater inflater = getMenuInflater();
977 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800978 mSave = menu.findItem(R.id.save);
979 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800980 return true;
981 }
982
983 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800984 public boolean onPrepareOptionsMenu(Menu menu) {
985 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -0800986 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800987 // Its possible there is a menu item OR a button.
988 boolean ccFieldVisible = mCc.isShown();
989 boolean bccFieldVisible = mBcc.isShown();
990 if (!ccFieldVisible || !bccFieldVisible) {
991 ccBcc.setVisible(true);
992 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
993 : R.string.add_bcc_label));
994 } else {
995 ccBcc.setVisible(false);
996 }
997 }
Mindy Pereira75f66632012-01-11 11:42:02 -0800998 if (mSave != null) {
999 mSave.setEnabled(shouldSave());
1000 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001001 return true;
1002 }
1003
1004 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001005 public boolean onOptionsItemSelected(MenuItem item) {
1006 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001007 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001008 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001009 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -08001010 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001011 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001012 case R.id.add_cc_bcc:
1013 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001014 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001015 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001016 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001017 break;
1018 case R.id.send:
1019 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001020 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001021 case R.id.discard:
1022 doDiscard();
1023 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001024 case R.id.settings:
1025 Utils.showSettings(this, mAccount);
1026 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001027 case android.R.id.home:
1028 finish();
1029 break;
1030 case R.id.help_info_menu_item:
1031 // TODO: enable context sensitive help
1032 Utils.showHelp(this, mAccount.helpIntentUri, null);
1033 break;
1034 case R.id.feedback_menu_item:
1035 Utils.sendFeedback(this, mAccount);
1036 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001037 default:
1038 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001039 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001040 }
1041 return !handled ? super.onOptionsItemSelected(item) : handled;
1042 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001043
Mindy Pereira33fe9082012-01-09 16:24:30 -08001044 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001045 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001046 }
1047
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001048 private void doSave(boolean showToast, boolean resetIME) {
1049 sendOrSaveWithSanityChecks(true, showToast, false);
1050 if (resetIME) {
1051 // Clear the IME composing suggestions from the body.
1052 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1053 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001054 }
1055
Mindy Pereira82cc5662012-01-09 17:29:30 -08001056 /*package*/ interface SendOrSaveCallback {
1057 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001058 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1059 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001060 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1061 }
1062
1063 /*package*/ static class SendOrSaveTask implements Runnable {
1064 private final Context mContext;
1065 private final SendOrSaveCallback mSendOrSaveCallback;
1066 @VisibleForTesting
1067 final SendOrSaveMessage mSendOrSaveMessage;
1068
1069 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1070 SendOrSaveCallback callback) {
1071 mContext = context;
1072 mSendOrSaveCallback = callback;
1073 mSendOrSaveMessage = message;
1074 }
1075
1076 @Override
1077 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001078 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001079
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001080 final Account selectedAccount = sendOrSaveMessage.mSelectedAccount;
1081 Message message = mSendOrSaveCallback.getMessage();
1082 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001083 // If a previous draft has been saved, in an account that is different
1084 // than what the user wants to send from, remove the old draft, and treat this
1085 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001086 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001087 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1088 ContentResolver resolver = mContext.getContentResolver();
1089 ContentValues values = new ContentValues();
1090 values.put(BaseColumns._ID, messageId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001091 if (selectedAccount.expungeMessageUri != null) {
1092 resolver.update(selectedAccount.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001093 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001094 } else {
1095 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001096 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001097 // reset messageId to 0, so a new message will be created
1098 messageId = UIProvider.INVALID_MESSAGE_ID;
1099 }
1100 }
1101
1102 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001103 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001104 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001105 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001106 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1107 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001108 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001109 ContentResolver resolver = mContext.getContentResolver();
1110 Uri messageUri = resolver.insert(
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001111 sendOrSaveMessage.mSave ? selectedAccount.saveDraftUri
1112 : selectedAccount.sendMessageUri, sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001113 if (sendOrSaveMessage.mSave && messageUri != null) {
1114 Cursor messageCursor = resolver.query(messageUri,
1115 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001116 if (messageCursor != null) {
1117 try {
1118 if (messageCursor.moveToFirst()) {
1119 // Broadcast notification that a new message has
1120 // been allocated
1121 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1122 new Message(messageCursor));
1123 }
1124 } finally {
1125 messageCursor.close();
1126 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001127 }
1128 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001129 }
1130
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001131 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001132 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001133 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001134 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001135 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001136 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001137 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001138 }
1139 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1140 }
1141 }
1142
1143 // Array of the outstanding send or save tasks. Access is synchronized
1144 // with the object itself
1145 /* package for testing */
1146 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
1147 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001148 private String mSignature;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001149
1150 /*package*/ static class SendOrSaveMessage {
1151 final Account mAccount;
1152 final Account mSelectedAccount;
1153 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001154 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001155 final boolean mSave;
1156 final int mRequestId;
1157
1158 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001159 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001160 mAccount = account;
1161 mSelectedAccount = selectedAccount;
1162 mValues = values;
1163 mRefMessageId = refMessageId;
1164 mSave = save;
1165 mRequestId = mValues.hashCode() ^ hashCode();
1166 }
1167
1168 int requestId() {
1169 return mRequestId;
1170 }
1171 }
1172
1173 /**
1174 * Get the to recipients.
1175 */
1176 public String[] getToAddresses() {
1177 return getAddressesFromList(mTo);
1178 }
1179
1180 /**
1181 * Get the cc recipients.
1182 */
1183 public String[] getCcAddresses() {
1184 return getAddressesFromList(mCc);
1185 }
1186
1187 /**
1188 * Get the bcc recipients.
1189 */
1190 public String[] getBccAddresses() {
1191 return getAddressesFromList(mBcc);
1192 }
1193
1194 public String[] getAddressesFromList(RecipientEditTextView list) {
1195 if (list == null) {
1196 return new String[0];
1197 }
1198 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1199 int count = tokens.length;
1200 String[] result = new String[count];
1201 for (int i = 0; i < count; i++) {
1202 result[i] = tokens[i].toString();
1203 }
1204 return result;
1205 }
1206
1207 /**
1208 * Check for invalid email addresses.
1209 * @param to String array of email addresses to check.
1210 * @param wrongEmailsOut Emails addresses that were invalid.
1211 */
1212 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
1213 for (String email : to) {
1214 if (!mValidator.isValid(email)) {
1215 wrongEmailsOut.add(email);
1216 }
1217 }
1218 }
1219
1220 /**
1221 * Show an error because the user has entered an invalid recipient.
1222 * @param message
1223 */
1224 public void showRecipientErrorDialog(String message) {
1225 // Only 1 invalid recipients error dialog should be allowed up at a
1226 // time.
1227 if (mRecipientErrorDialog != null) {
1228 mRecipientErrorDialog.dismiss();
1229 }
1230 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1231 R.string.recipient_error_dialog_title)
1232 .setIconAttribute(android.R.attr.alertDialogIcon)
1233 .setCancelable(false)
1234 .setPositiveButton(
1235 R.string.ok, new Dialog.OnClickListener() {
1236 public void onClick(DialogInterface dialog, int which) {
1237 // after the user dismisses the recipient error
1238 // dialog we want to make sure to refocus the
1239 // recipient to field so they can fix the issue
1240 // easily
1241 if (mTo != null) {
1242 mTo.requestFocus();
1243 }
1244 mRecipientErrorDialog = null;
1245 }
1246 }).show();
1247 }
1248
1249 /**
1250 * Update the state of the UI based on whether or not the current draft
1251 * needs to be saved and the message is not empty.
1252 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001253 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001254 if (mSave != null) {
1255 mSave.setEnabled((shouldSave() && !isBlank()));
1256 }
1257 }
1258
1259 /**
1260 * Returns true if we need to save the current draft.
1261 */
1262 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001263 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001264 // The message should only be saved if:
1265 // It hasn't been sent AND
1266 // Some text has been added to the message OR
1267 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001268 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -08001269 (mReplyFromChanged && !isBlank()));
1270 }
1271 }
1272
1273 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001274 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001275 * @return boolean
1276 */
1277 public boolean isBlank() {
1278 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001279 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1280 mBodyView.getText().toString()) == 0)
1281 && mTo.length() == 0
1282 && mCc.length() == 0 && mBcc.length() == 0
1283 && mAttachmentsView.getAttachments().size() == 0;
1284 }
1285
1286 @VisibleForTesting
1287 protected int getSignatureStartPosition(String signature, String bodyText) {
1288 int startPos = -1;
1289
1290 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1291 return startPos;
1292 }
1293
1294 int bodyLength = bodyText.length();
1295 int signatureLength = signature.length();
1296 String printableVersion = convertToPrintableSignature(signature);
1297 int printableLength = printableVersion.length();
1298
1299 if (bodyLength >= printableLength
1300 && bodyText.substring(bodyLength - printableLength)
1301 .equals(printableVersion)) {
1302 startPos = bodyLength - printableLength;
1303 } else if (bodyLength >= signatureLength
1304 && bodyText.substring(bodyLength - signatureLength)
1305 .equals(signature)) {
1306 startPos = bodyLength - signatureLength;
1307 }
1308 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001309 }
1310
1311 /**
1312 * Allows any changes made by the user to be ignored. Called when the user
1313 * decides to discard a draft.
1314 */
1315 private void discardChanges() {
1316 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001317 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001318 mReplyFromChanged = false;
1319 }
1320
1321 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001322 * @param body
1323 * @param save
1324 * @param showToast
1325 * @return Whether the send or save succeeded.
1326 */
1327 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1328 final boolean orientationChanged) {
1329 String[] to, cc, bcc;
1330 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001331
Mindy Pereira181df782012-03-01 13:32:44 -08001332 if (orientationChanged) {
1333 to = cc = bcc = new String[0];
1334 } else {
1335 to = getToAddresses();
1336 cc = getCcAddresses();
1337 bcc = getBccAddresses();
1338 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001339
Mindy Pereira181df782012-03-01 13:32:44 -08001340 // Don't let the user send to nobody (but it's okay to save a message
1341 // with no recipients)
1342 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1343 showRecipientErrorDialog(getString(R.string.recipient_needed));
1344 return false;
1345 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001346
Mindy Pereira181df782012-03-01 13:32:44 -08001347 List<String> wrongEmails = new ArrayList<String>();
1348 if (!save) {
1349 checkInvalidEmails(to, wrongEmails);
1350 checkInvalidEmails(cc, wrongEmails);
1351 checkInvalidEmails(bcc, wrongEmails);
1352 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001353
Mindy Pereira181df782012-03-01 13:32:44 -08001354 // Don't let the user send an email with invalid recipients
1355 if (wrongEmails.size() > 0) {
1356 String errorText = String.format(getString(R.string.invalid_recipient),
1357 wrongEmails.get(0));
1358 showRecipientErrorDialog(errorText);
1359 return false;
1360 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001361
Mindy Pereira181df782012-03-01 13:32:44 -08001362 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
1363 public void onClick(DialogInterface dialog, int which) {
1364 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1365 }
1366 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001367
Mindy Pereira181df782012-03-01 13:32:44 -08001368 // Show a warning before sending only if there are no attachments.
1369 if (!save) {
1370 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1371 boolean warnAboutEmptySubject = isSubjectEmpty();
1372 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001373
Mindy Pereira181df782012-03-01 13:32:44 -08001374 // A warning about an empty body may not be warranted when
1375 // forwarding mails, since a common use case is to forward
1376 // quoted text and not append any more text.
1377 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001378
Mindy Pereira181df782012-03-01 13:32:44 -08001379 // When we bring up a dialog warning the user about a send,
1380 // assume that they accept sending the message. If they do not,
1381 // the dialog listener is required to enable sending again.
1382 if (warnAboutEmptySubject) {
1383 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1384 return true;
1385 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001386
Mindy Pereira181df782012-03-01 13:32:44 -08001387 if (warnAboutEmptyBody) {
1388 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1389 return true;
1390 }
1391 }
1392 // Ask for confirmation to send (if always required)
1393 if (showSendConfirmation()) {
1394 showSendConfirmDialog(R.string.confirm_send_message, listener);
1395 return true;
1396 }
1397 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001398
Mindy Pereira181df782012-03-01 13:32:44 -08001399 sendOrSave(body, save, showToast, false);
1400 return true;
1401 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001402
Mindy Pereira181df782012-03-01 13:32:44 -08001403 /**
1404 * Returns a boolean indicating whether warnings should be shown for empty
1405 * subject and body fields
1406 *
1407 * @return True if a warning should be shown for empty text fields
1408 */
1409 protected boolean showEmptyTextWarnings() {
1410 return mAttachmentsView.getAttachments().size() == 0;
1411 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001412
Mindy Pereira181df782012-03-01 13:32:44 -08001413 /**
1414 * Returns a boolean indicating whether the user should confirm each send
1415 *
1416 * @return True if a warning should be on each send
1417 */
1418 protected boolean showSendConfirmation() {
1419 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1420 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001421
Mindy Pereira181df782012-03-01 13:32:44 -08001422 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1423 if (mSendConfirmDialog != null) {
1424 mSendConfirmDialog.dismiss();
1425 mSendConfirmDialog = null;
1426 }
1427 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1428 .setTitle(R.string.confirm_send_title)
1429 .setIconAttribute(android.R.attr.alertDialogIcon)
1430 .setPositiveButton(R.string.send, listener)
1431 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1432 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001433
Mindy Pereira181df782012-03-01 13:32:44 -08001434 /**
1435 * Returns whether the ComposeArea believes there is any text in the body of
1436 * the composition. TODO: When ComposeArea controls the Body as well, add
1437 * that here.
1438 */
1439 public boolean isBodyEmpty() {
1440 return !mQuotedTextView.isTextIncluded();
1441 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001442
Mindy Pereira181df782012-03-01 13:32:44 -08001443 /**
1444 * Test to see if the subject is empty.
1445 *
1446 * @return boolean.
1447 */
1448 // TODO: this will likely go away when composeArea.focus() is implemented
1449 // after all the widget control is moved over.
1450 public boolean isSubjectEmpty() {
1451 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1452 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001453
Mindy Pereira181df782012-03-01 13:32:44 -08001454 /* package */
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001455 static int sendOrSaveInternal(Context context, final Account account,
1456 final Account selectedAccount, String fromAddress, final Spanned body,
1457 final String[] to, final String[] cc, final String[] bcc, final String subject,
1458 final CharSequence quotedText, final List<Attachment> attachments,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001459 final String refMessageId, SendOrSaveCallback callback, Handler handler, boolean save,
Mindy Pereira12575862012-03-21 16:30:54 -07001460 int composeMode) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001461 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001462
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001463 MessageModification.putToAddresses(values, to);
1464 MessageModification.putCcAddresses(values, cc);
1465 MessageModification.putBccAddresses(values, bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001466
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001467 MessageModification.putSubject(values, subject);
1468 String htmlBody = Html.toHtml(body);
1469 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1470 StringBuilder fullBody = new StringBuilder(htmlBody);
1471 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001472 // HTML gets converted to text for now
1473 final String text = quotedText.toString();
1474 if (QuotedTextView.containsQuotedText(text)) {
1475 int pos = QuotedTextView.getQuotedTextOffset(text);
1476 fullBody.append(text.substring(0, pos));
Paul Westbrookffe6bb02012-03-27 11:50:12 -07001477 MessageModification.putQuoteStartPos(values, fullBody.length());
Mindy Pereira12575862012-03-21 16:30:54 -07001478 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07001479 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001480 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001481 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1482 // This shouldn't happen, but just use what we have,
1483 // and don't do server-side expansion
1484 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001485 }
1486 }
Mindy Pereira12575862012-03-21 16:30:54 -07001487 int draftType = -1;
1488 switch (composeMode) {
1489 case ComposeActivity.COMPOSE:
1490 draftType = DraftType.COMPOSE;
1491 break;
1492 case ComposeActivity.REPLY:
1493 draftType = DraftType.REPLY;
1494 break;
1495 case ComposeActivity.REPLY_ALL:
1496 draftType = DraftType.REPLY_ALL;
1497 break;
1498 case ComposeActivity.FORWARD:
1499 draftType = DraftType.FORWARD;
1500 break;
1501 }
1502 MessageModification.putDraftType(values, draftType);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001503 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1504 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereiraf944e962012-01-17 11:43:36 -08001505 MessageModification.putAttachments(values, attachments);
Mindy Pereira12575862012-03-21 16:30:54 -07001506 if (!TextUtils.isEmpty(refMessageId)) {
1507 MessageModification.putRefMessageId(values, refMessageId);
1508 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001509
Mindy Pereira181df782012-03-01 13:32:44 -08001510 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1511 values, refMessageId, save);
1512 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001513
Mindy Pereira181df782012-03-01 13:32:44 -08001514 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001515
Mindy Pereira181df782012-03-01 13:32:44 -08001516 // Do the send/save action on the specified handler to avoid possible
1517 // ANRs
1518 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001519
Mindy Pereira181df782012-03-01 13:32:44 -08001520 return sendOrSaveMessage.requestId();
1521 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001522
Mindy Pereira181df782012-03-01 13:32:44 -08001523 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1524 boolean orientationChanged) {
1525 // Check if user is a monkey. Monkeys can compose and hit send
1526 // button but are not allowed to send anything off the device.
1527 if (!save && ActivityManager.isUserAMonkey()) {
1528 return;
1529 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001530
Mindy Pereira181df782012-03-01 13:32:44 -08001531 String[] to, cc, bcc;
1532 if (orientationChanged) {
1533 to = cc = bcc = new String[0];
1534 } else {
1535 to = getToAddresses();
1536 cc = getCcAddresses();
1537 bcc = getBccAddresses();
1538 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001539
Mindy Pereira181df782012-03-01 13:32:44 -08001540 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001541 private int mRestoredRequestId;
1542
1543 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08001544 synchronized (mActiveTasks) {
1545 int numTasks = mActiveTasks.size();
1546 if (numTasks == 0) {
1547 // Start service so we won't be killed if this app is
1548 // put in the background.
1549 startService(new Intent(ComposeActivity.this, EmptyService.class));
1550 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001551
Mindy Pereira181df782012-03-01 13:32:44 -08001552 mActiveTasks.add(sendOrSaveTask);
1553 }
1554 if (sTestSendOrSaveCallback != null) {
1555 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1556 }
1557 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001558
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001559 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1560 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08001561 synchronized (mDraftLock) {
1562 mDraftId = message.id;
1563 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001564 if (sRequestMessageIdMap != null) {
1565 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1566 }
Mindy Pereira181df782012-03-01 13:32:44 -08001567 // Cache request message map, in case the process is killed
1568 saveRequestMap();
1569 }
1570 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001571 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08001572 }
1573 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001574
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001575 public Message getMessage() {
1576 synchronized (mDraftLock) {
1577 return mDraft;
1578 }
1579 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001580
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001581 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1582 if (success) {
1583 // Successfully sent or saved so reset change markers
1584 discardChanges();
1585 } else {
1586 // A failure happened with saving/sending the draft
1587 // TODO(pwestbro): add a better string that should be used
1588 // when failing to send or save
1589 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1590 .show();
1591 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001592
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001593 int numTasks;
1594 synchronized (mActiveTasks) {
1595 // Remove the task from the list of active tasks
1596 mActiveTasks.remove(task);
1597 numTasks = mActiveTasks.size();
1598 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001599
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001600 if (numTasks == 0) {
1601 // Stop service so we can be killed.
1602 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1603 }
1604 if (sTestSendOrSaveCallback != null) {
1605 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1606 }
1607 }
Mindy Pereira181df782012-03-01 13:32:44 -08001608 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001609
Mindy Pereira181df782012-03-01 13:32:44 -08001610 // Get the selected account if the from spinner has been setup.
1611 Account selectedAccount = mAccount;
1612 String fromAddress = selectedAccount.name;
1613 if (selectedAccount == null || fromAddress == null) {
1614 // We don't have either the selected account or from address,
1615 // use mAccount.
1616 selectedAccount = mAccount;
1617 fromAddress = mAccount.name;
1618 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001619
Mindy Pereira181df782012-03-01 13:32:44 -08001620 if (mSendSaveTaskHandler == null) {
1621 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1622 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001623
Mindy Pereira181df782012-03-01 13:32:44 -08001624 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1625 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001626
Mindy Pereira12575862012-03-21 16:30:54 -07001627 String refMessageString = mRefMessage != null ? mRefMessage.uri.toString() : "";
Mindy Pereira181df782012-03-01 13:32:44 -08001628 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body, to, cc,
Paul Westbrookffe6bb02012-03-27 11:50:12 -07001629 bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedTextIfIncluded(),
Mindy Pereira12575862012-03-21 16:30:54 -07001630 mAttachmentsView.getAttachments(), refMessageString, callback,
1631 mSendSaveTaskHandler, save, mComposeMode);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001632
Mindy Pereira181df782012-03-01 13:32:44 -08001633 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1634 mRecipient = selectedAccount.name;
1635 }
1636 mAccount = selectedAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001637
Mindy Pereira181df782012-03-01 13:32:44 -08001638 // Don't display the toast if the user is just changing the orientation,
1639 // but we still need to save the draft to the cursor because this is how we restore
1640 // the attachments when the configuration change completes.
1641 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1642 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1643 Toast.LENGTH_LONG).show();
1644 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001645
Mindy Pereira181df782012-03-01 13:32:44 -08001646 // Need to update variables here because the send or save completes
1647 // asynchronously even though the toast shows right away.
1648 discardChanges();
1649 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001650
Mindy Pereira181df782012-03-01 13:32:44 -08001651 // If we are sending, finish the activity
1652 if (!save) {
1653 finish();
1654 }
1655 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001656
Mindy Pereira181df782012-03-01 13:32:44 -08001657 /**
1658 * Save the state of the request messageid map. This allows for the Gmail
1659 * process to be killed, but and still allow for ComposeActivity instances
1660 * to be recreated correctly.
1661 */
1662 private void saveRequestMap() {
1663 // TODO: store the request map in user preferences.
1664 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001665
Mindy Pereira013194c2012-01-06 15:09:33 -08001666 public void doAttach() {
1667 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1668 i.addCategory(Intent.CATEGORY_OPENABLE);
Mindy Pereira181df782012-03-01 13:32:44 -08001669 if (android.provider.Settings.System.getInt(getContentResolver(),
1670 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08001671 i.setType("*/*");
1672 } else {
1673 i.setType("image/*");
1674 }
1675 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08001676 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
1677 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08001678 }
1679
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001680 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001681 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001682 if (mCcBccButton != null) {
1683 mCcBccButton.setVisibility(View.GONE);
1684 }
1685 }
1686
Mindy Pereira326c6602012-01-04 15:32:42 -08001687 @Override
1688 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001689 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001690 if (position == ComposeActivity.REPLY) {
1691 mComposeMode = ComposeActivity.REPLY;
1692 } else if (position == ComposeActivity.REPLY_ALL) {
1693 mComposeMode = ComposeActivity.REPLY_ALL;
1694 } else if (position == ComposeActivity.FORWARD) {
1695 mComposeMode = ComposeActivity.FORWARD;
1696 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001697 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001698 resetMessageForModeChange();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001699 if (mRefMessage != null) {
1700 initFromRefMessage(mComposeMode, mAccount.name);
1701 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001702 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001703 return true;
1704 }
1705
Mindy Pereira154386a2012-01-11 13:02:33 -08001706 private void resetMessageForModeChange() {
1707 // When switching between reply, reply all, forward,
1708 // follow the behavior of webview.
1709 // The contents of the following fields are cleared
1710 // so that they can be populated directly from the
1711 // ref message:
1712 // 1) Any recipient fields
1713 // 2) The subject
1714 mTo.setText("");
1715 mCc.setText("");
1716 mBcc.setText("");
1717 // Any edits to the subject are replaced with the original subject.
1718 mSubject.setText("");
1719
1720 // Any changes to the contents of the following fields are kept:
1721 // 1) Body
1722 // 2) Attachments
1723 // If the user made changes to attachments, keep their changes.
1724 if (!mAttachmentsChanged) {
1725 mAttachmentsView.deleteAllAttachments();
1726 }
1727 }
1728
Mindy Pereira326c6602012-01-04 15:32:42 -08001729 private class ComposeModeAdapter extends ArrayAdapter<String> {
1730
1731 private LayoutInflater mInflater;
1732
1733 public ComposeModeAdapter(Context context) {
1734 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1735 .getStringArray(R.array.compose_modes));
1736 }
1737
1738 private LayoutInflater getInflater() {
1739 if (mInflater == null) {
1740 mInflater = LayoutInflater.from(getContext());
1741 }
1742 return mInflater;
1743 }
1744
1745 @Override
1746 public View getView(int position, View convertView, ViewGroup parent) {
1747 if (convertView == null) {
1748 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1749 }
1750 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1751 return super.getView(position, convertView, parent);
1752 }
1753 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001754
1755 @Override
1756 public void onRespondInline(String text) {
1757 appendToBody(text, false);
1758 }
1759
1760 /**
1761 * Append text to the body of the message. If there is no existing body
1762 * text, just sets the body to text.
1763 *
1764 * @param text
1765 * @param withSignature True to append a signature.
1766 */
1767 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001768 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001769 if (bodyText != null && bodyText.length() > 0) {
1770 bodyText.append(text);
1771 } else {
1772 setBody(text, withSignature);
1773 }
1774 }
1775
1776 /**
1777 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001778 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001779 * @param text
1780 * @param withSignature True to append a signature.
1781 */
1782 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001783 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001784 if (withSignature) {
1785 appendSignature();
1786 }
1787 }
1788
1789 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07001790 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
1791 if (!TextUtils.equals(newSignature, mSignature)) {
1792 mSignature = newSignature;
1793 if (!TextUtils.isEmpty(mSignature)
1794 && getSignatureStartPosition(mSignature,
1795 mBodyView.getText().toString()) < 0) {
1796 // Appending a signature does not count as changing text.
1797 mBodyView.removeTextChangedListener(this);
1798 mBodyView.append(convertToPrintableSignature(mSignature));
1799 mBodyView.addTextChangedListener(this);
1800 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001801 }
1802 }
1803
1804 private String convertToPrintableSignature(String signature) {
1805 String signatureResource = getResources().getString(R.string.signature);
1806 if (signature == null) {
1807 signature = "";
1808 }
1809 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001810 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001811
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001812 @Override
1813 public void onAccountChanged() {
1814 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereira181df782012-03-01 13:32:44 -08001815 if (!mAccount.equals(selectedAccountInfo)) {
1816 mAccount = selectedAccountInfo;
1817 mCachedSettings = null;
1818 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
1819 // TODO: handle discarding attachments when switching accounts.
1820 // Only enable save for this draft if there is any other content
1821 // in the message.
1822 if (!isBlank()) {
1823 enableSave(true);
1824 }
1825 mReplyFromChanged = true;
1826 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001827 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001828 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001829
1830 public void enableSave(boolean enabled) {
1831 if (mSave != null) {
1832 mSave.setEnabled(enabled);
1833 }
1834 }
1835
1836 public void enableSend(boolean enabled) {
1837 if (mSend != null) {
1838 mSend.setEnabled(enabled);
1839 }
1840 }
1841
1842 /**
1843 * Handles button clicks from any error dialogs dealing with sending
1844 * a message.
1845 */
1846 @Override
1847 public void onClick(DialogInterface dialog, int which) {
1848 switch (which) {
1849 case DialogInterface.BUTTON_POSITIVE: {
1850 doDiscardWithoutConfirmation(true /* show toast */ );
1851 break;
1852 }
1853 case DialogInterface.BUTTON_NEGATIVE: {
1854 // If the user cancels the send, re-enable the send button.
1855 enableSend(true);
1856 break;
1857 }
1858 }
1859
1860 }
1861
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001862 private void doDiscard() {
1863 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
1864 .setPositiveButton(R.string.ok, this)
1865 .setNegativeButton(R.string.cancel, null)
1866 .create().show();
1867 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001868 /**
1869 * Effectively discard the current message.
1870 *
1871 * This method is either invoked from the menu or from the dialog
1872 * once the user has confirmed that they want to discard the message.
1873 * @param showToast show "Message discarded" toast if true
1874 */
1875 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001876 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001877 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1878 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07001879 values.put(BaseColumns._ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001880 if (mAccount.expungeMessageUri != null) {
1881 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
1882 } else {
1883 // TODO(mindyp): call delete on this conversation instead.
1884 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001885 // This is not strictly necessary (since we should not try to
1886 // save the draft after calling this) but it ensures that if we
1887 // do save again for some reason we make a new draft rather than
1888 // trying to resave an expunged draft.
1889 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1890 }
1891 }
1892
1893 if (showToast) {
1894 // Display a toast to let the user know
1895 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1896 }
1897
1898 // This prevents the draft from being saved in onPause().
1899 discardChanges();
1900 finish();
1901 }
1902
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001903 private void saveIfNeeded() {
1904 if (mAccount == null) {
1905 // We have not chosen an account yet so there's no way that we can save. This is ok,
1906 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1907 // user has not interacted with us yet and there is no real state to save.
1908 return;
1909 }
1910
1911 if (shouldSave()) {
1912 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1913 }
1914 }
1915
1916 private void saveIfNeededOnOrientationChanged() {
1917 if (mAccount == null) {
1918 // We have not chosen an account yet so there's no way that we can save. This is ok,
1919 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1920 // user has not interacted with us yet and there is no real state to save.
1921 return;
1922 }
1923
1924 if (shouldSave()) {
1925 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1926 }
1927 }
1928
1929 /**
1930 * Save a draft if a draft already exists or the message is not empty.
1931 */
1932 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1933 saveOnOrientationChanged();
1934 if (resetIME) {
1935 // Clear the IME composing suggestions from the body.
1936 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1937 }
1938 }
1939
1940 protected boolean saveOnOrientationChanged() {
1941 return sendOrSaveWithSanityChecks(true, false, true);
1942 }
1943
1944 @Override
1945 public void onAttachmentDeleted() {
1946 mAttachmentsChanged = true;
1947 updateSaveUi();
1948 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001949
1950
1951 /**
1952 * This is called any time one of our text fields changes.
1953 */
1954 public void afterTextChanged(Editable s) {
1955 mTextChanged = true;
1956 updateSaveUi();
1957 }
1958
1959 @Override
1960 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1961 // Do nothing.
1962 }
1963
1964 public void onTextChanged(CharSequence s, int start, int before, int count) {
1965 // Do nothing.
1966 }
1967
1968
1969 // There is a big difference between the text associated with an address changing
1970 // to add the display name or to format properly and a recipient being added or deleted.
1971 // Make sure we only notify of changes when a recipient has been added or deleted.
1972 private class RecipientTextWatcher implements TextWatcher {
1973 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
1974
1975 private RecipientEditTextView mView;
1976
1977 private TextWatcher mListener;
1978
1979 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
1980 mView = view;
1981 mListener = listener;
1982 }
1983
1984 @Override
1985 public void afterTextChanged(Editable s) {
1986 if (hasChanged()) {
1987 mListener.afterTextChanged(s);
1988 }
1989 }
1990
1991 private boolean hasChanged() {
1992 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
1993 int totalCount = currRecips.length;
1994 int totalPrevCount = 0;
1995 for (Entry<String, Integer> entry : mContent.entrySet()) {
1996 totalPrevCount += entry.getValue();
1997 }
1998 if (totalCount != totalPrevCount) {
1999 return true;
2000 }
2001
2002 for (String recip : currRecips) {
2003 if (!mContent.containsKey(recip)) {
2004 return true;
2005 } else {
2006 int count = mContent.get(recip) - 1;
2007 if (count < 0) {
2008 return true;
2009 } else {
2010 mContent.put(recip, count);
2011 }
2012 }
2013 }
2014 return false;
2015 }
2016
2017 private String[] tokenizeRecips(String[] recips) {
2018 // Tokenize them all and put them in the list.
2019 String[] recipAddresses = new String[recips.length];
2020 for (int i = 0; i < recips.length; i++) {
2021 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2022 }
2023 return recipAddresses;
2024 }
2025
2026 @Override
2027 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2028 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2029 for (String recip : recips) {
2030 if (!mContent.containsKey(recip)) {
2031 mContent.put(recip, 1);
2032 } else {
2033 mContent.put(recip, (mContent.get(recip)) + 1);
2034 }
2035 }
2036 }
2037
2038 @Override
2039 public void onTextChanged(CharSequence s, int start, int before, int count) {
2040 // Do nothing.
2041 }
2042 }
Mindy Pereira181df782012-03-01 13:32:44 -08002043
2044 @Override
2045 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
2046 if (id == ACCOUNT_SETTINGS_LOADER) {
Mindy Pereira23e9fde2012-03-20 15:08:24 -07002047 if (mAccount != null && mAccount.settingsQueryUri != null) {
Mindy Pereira181df782012-03-01 13:32:44 -08002048 return new CursorLoader(this, mAccount.settingsQueryUri,
2049 UIProvider.SETTINGS_PROJECTION, null, null, null);
2050 }
2051 }
2052 return null;
2053 }
2054
2055 @Override
2056 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
2057 if (loader.getId() == ACCOUNT_SETTINGS_LOADER) {
2058 if (data != null) {
2059 data.moveToFirst();
2060 mCachedSettings = new Settings(data);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002061 appendSignature();
Mindy Pereira181df782012-03-01 13:32:44 -08002062 }
2063 }
2064 }
2065
2066 @Override
2067 public void onLoaderReset(Loader<Cursor> loader) {
2068 // Do nothing.
2069 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08002070}