blob: 23a0e83ecd326c806385795c05886864d3c9a86b [file] [log] [blame]
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Mindy Pereira326c6602012-01-04 15:32:42 -080019import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070020import android.app.ActionBar.OnNavigationListener;
21import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080022import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.Dialog;
Mindy Pereira6349a042012-01-04 11:25:01 -080025import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080026import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080027import android.content.Context;
Mindy Pereira82cc5662012-01-09 17:29:30 -080028import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080029import android.content.Intent;
Mindy Pereira82cc5662012-01-09 17:29:30 -080030import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080031import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080033import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080034import android.os.Handler;
35import android.os.HandlerThread;
Paul Westbrookf97588b2012-03-20 11:11:37 -070036import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080037import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080038import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.text.Html;
40import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080041import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080042import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080043import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080044import android.text.util.Rfc822Tokenizer;
Mindy Pereira326c6602012-01-04 15:32:42 -080045import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080046import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080049import android.view.View;
50import android.view.View.OnClickListener;
Andrew Sapperstein2ea06182012-06-19 17:23:29 -070051import android.view.View.OnFocusChangeListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070052import android.view.ViewGroup;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080053import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080054import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080055import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070056import android.widget.EditText;
Mindy Pereira1f936682012-03-02 11:30:33 -080057import android.widget.ImageView;
Mindy Pereira6349a042012-01-04 11:25:01 -080058import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080059import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080060
Mindy Pereirac17d0732011-12-29 10:46:19 -080061import com.android.common.Rfc822Validator;
Andy Huang5c5fd572012-04-08 18:19:29 -070062import com.android.ex.chips.RecipientEditTextView;
63import com.android.mail.R;
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 Pereira92551d02012-04-05 11:31:12 -070073import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080074import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080075import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070076import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070077import com.android.mail.providers.UIProvider.DraftType;
Paul Westbrook92227f62012-03-20 10:32:51 -070078import com.android.mail.utils.AccountUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -070079import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -080080import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080081import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080082import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080083import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080084import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080085
Mindy Pereira62de1b12012-04-06 12:17:56 -070086import org.json.JSONException;
87
Mindy Pereira8eca57a2012-03-20 16:42:34 -070088import java.io.UnsupportedEncodingException;
89import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080090import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070091import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080092import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080093import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080094import java.util.HashSet;
95import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -070096import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070097import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -080098import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080099
100public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800101 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Andrew Sapperstein226e4fd2012-06-20 15:09:09 -0700102 AttachmentDeletedListener, OnAccountChangedListener {
Mindy Pereira6349a042012-01-04 11:25:01 -0800103 // Identifiers for which type of composition this is
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700104 static final int COMPOSE = -1;
105 static final int REPLY = 0;
106 static final int REPLY_ALL = 1;
107 static final int FORWARD = 2;
108 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800109
110 // Integer extra holding one of the above compose action
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700111 private static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800112
Mindy Pereira326689d2012-05-17 10:14:14 -0700113 private static final String EXTRA_SHOW_CC = "showCc";
114 private static final String EXTRA_SHOW_BCC = "showBcc";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700115
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700116 private static final String UTF8_ENCODING_NAME = "UTF-8";
117
118 private static final String MAIL_TO = "mailto";
119
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700120 private static final String EXTRA_SUBJECT = "subject";
121
122 private static final String EXTRA_BODY = "body";
123
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700124 private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
125
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700126 // Extra that we can get passed from other activities
127 private static final String EXTRA_TO = "to";
128 private static final String EXTRA_CC = "cc";
129 private static final String EXTRA_BCC = "bcc";
130
131 // List of all the fields
132 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
133
Mindy Pereira82cc5662012-01-09 17:29:30 -0800134 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
135 // Map containing information about requests to create new messages, and the id of the
136 // messages that were the result of those requests.
137 //
138 // This map is used when the activity that initiated the save a of a new message, is killed
139 // before the save has completed (and when we know the id of the newly created message). When
140 // a save is completed, the service that is running in the background, will update the map
141 //
142 // When a new ComposeActivity instance is created, it will attempt to use the information in
143 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
144 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
145 // to populate the map with data stored in shared preferences.
146 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
147 // Key used to store the above map
148 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800149 /**
150 * Notifies the {@code Activity} that the caller is an Email
151 * {@code Activity}, so that the back behavior may be modified accordingly.
152 *
153 * @see #onAppUpPressed
154 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700155 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800156
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700157 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700158
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800159 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700160 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700161 // If this is an action to edit an existing draft messagge, this extra will hold the
162 // draft message
163 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800164 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700165 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800166 // Request numbers for activities we start
167 private static final int RESULT_PICK_ATTACHMENT = 1;
168 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700169 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700170 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700171
172 // Max size for attachments (5 megs). Will be overridden by account settings if found.
173 // TODO(mindyp): read this from account settings?
174 private static final int DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700175 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
176 private static final String EXTRA_REQUEST_ID = "requestId";
177 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
178 private static final String EXTRA_FOCUS_SELECTION_END = null;
179 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800180
Mindy Pereira82cc5662012-01-09 17:29:30 -0800181 /**
182 * A single thread for running tasks in the background.
183 */
184 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800185 private RecipientEditTextView mTo;
186 private RecipientEditTextView mCc;
187 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800188 private Button mCcBccButton;
189 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800190 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800191 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700192 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800193 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800194 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800195 private TextView mSubject;
196
Mindy Pereira326c6602012-01-04 15:32:42 -0800197 private ComposeModeAdapter mComposeModeAdapter;
198 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800199 private boolean mForward;
200 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800201 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700202 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800203 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800204 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800205 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700206 @VisibleForTesting
207 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800208 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800209 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800210 private boolean mTextChanged;
211 private boolean mReplyFromChanged;
212 private MenuItem mSave;
213 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800214 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800215 private AlertDialog mSendConfirmDialog;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700216 @VisibleForTesting
217 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800218 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
219 private Message mDraft;
220 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800221 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800222
Mindy Pereira326c6602012-01-04 15:32:42 -0800223 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700224 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
225 */
226 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700227 private RecipientTextWatcher mToListener;
228 private RecipientTextWatcher mCcListener;
229 private RecipientTextWatcher mBccListener;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700230
231
232 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800233 * Can be called from a non-UI thread.
234 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800235 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700236 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800237 }
238
Mindy Pereira6349a042012-01-04 11:25:01 -0800239 /**
240 * Can be called from a non-UI thread.
241 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800242 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800243 launch(launcher, account, null, COMPOSE);
244 }
245
246 /**
247 * Can be called from a non-UI thread.
248 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800249 public static void reply(Context launcher, Account account, Message message) {
250 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800251 }
252
253 /**
254 * Can be called from a non-UI thread.
255 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800256 public static void replyAll(Context launcher, Account account, Message message) {
257 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800258 }
259
260 /**
261 * Can be called from a non-UI thread.
262 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800263 public static void forward(Context launcher, Account account, Message message) {
264 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800265 }
266
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800267 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800268 Intent intent = new Intent(launcher, ComposeActivity.class);
269 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
270 intent.putExtra(EXTRA_ACTION, action);
271 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700272 if (action == EDIT_DRAFT) {
273 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
274 } else {
275 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
276 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800277 launcher.startActivity(intent);
278 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800279
280 @Override
281 public void onCreate(Bundle savedInstanceState) {
282 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800283 setContentView(R.layout.compose);
284 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800285 Intent intent = getIntent();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700286 Account account = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700287 Message message;
Mindy Pereira71c9e562012-05-17 11:01:02 -0700288 boolean showQuotedText = false;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700289 int action;
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700290 Object accountExtra = intent != null && intent.getExtras() != null ? intent.getExtras()
291 .get(Utils.EXTRA_ACCOUNT) : null;
292 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700293 if (hadSavedInstanceStateMessage(savedInstanceState)) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700294 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
295 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
296 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
297 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
298 } else {
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700299 if (accountExtra instanceof Account) {
300 account = (Account) intent.getExtras().get(Utils.EXTRA_ACCOUNT);
301 } else if (accountExtra instanceof String) {
302 // For backwards compatibility
303 String extraAccount = intent.getStringExtra(Utils.EXTRA_ACCOUNT);
304 if (syncingAccounts.length > 0) {
305 if (!TextUtils.isEmpty(extraAccount)) {
306 for (Account a : syncingAccounts) {
307 if (a.name.equals(extraAccount)) {
308 account = a;
309 }
310 }
311 }
312 }
313 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700314 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
315 // Initialize the message from the message in the intent
316 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
317 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
318 }
Paul Westbrook92227f62012-03-20 10:32:51 -0700319 if (account == null) {
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700320 if (syncingAccounts != null && syncingAccounts.length > 0) {
321 account = syncingAccounts[0];
Paul Westbrook92227f62012-03-20 10:32:51 -0700322 }
323 }
324
325 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800326 if (mAccount == null) {
327 return;
328 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700329
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700330 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
331 mLaunchedFromEmail = true;
332 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
333 final Uri dataUri = intent.getData();
334 if (dataUri != null) {
335 final String dataScheme = intent.getData().getScheme();
336 final String accountScheme = mAccount.composeIntentUri.getScheme();
337 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
338 }
339 }
340
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700341 if (message != null && action != EDIT_DRAFT) {
342 initFromDraftMessage(message);
343 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700344 showCcBcc(savedInstanceState);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700345 showQuotedText = message.appendRefMessageContent;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700346 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700347 initFromDraftMessage(message);
Mindy Pereiraef388302012-06-18 19:07:44 -0700348 boolean showBcc = !TextUtils.isEmpty(message.bcc);
349 boolean showCc = showBcc || !TextUtils.isEmpty(message.cc);
350 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700351 // Update the action to the draft type of the previous draft
352 switch (message.draftType) {
353 case UIProvider.DraftType.REPLY:
354 action = REPLY;
355 break;
356 case UIProvider.DraftType.REPLY_ALL:
357 action = REPLY_ALL;
358 break;
359 case UIProvider.DraftType.FORWARD:
360 action = FORWARD;
361 break;
362 case UIProvider.DraftType.COMPOSE:
363 default:
364 action = COMPOSE;
365 break;
366 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700367 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700368 showQuotedText = message.appendRefMessageContent;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700369 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
370 if (mRefMessage != null) {
371 initFromRefMessage(action, mAccount.name);
Mindy Pereiraef388302012-06-18 19:07:44 -0700372 if (mRefMessage != null) {
373 // CC field only gets populated when doing REPLY_ALL.
374 // BCC never gets auto-populated, unless the user is editing
375 // a draft with one.
376 if (!TextUtils.isEmpty(mRefMessage.cc) && action == REPLY_ALL) {
377 mCcBccView.show(false, true, false);
378 }
379 }
380 updateHideOrShowCcBcc();
Mindy Pereira71c9e562012-05-17 11:01:02 -0700381 showQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700382 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700383 } else {
384 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700385 }
386
387 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800388 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800389 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800390 initRecipients();
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700391 // Don't bother with the intent if we have procured a message from the
392 // intent already.
393 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
394 initAttachmentsFromIntent(intent);
395 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800396 initActionBar(action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700397 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
398 action);
Mindy Pereira75f66632012-01-11 11:42:02 -0800399 initChangeListeners();
Mindy Pereira433b1982012-04-03 11:53:07 -0700400 setFocus(action);
Mindy Pereira326689d2012-05-17 10:14:14 -0700401 updateHideOrShowCcBcc();
Mindy Pereira71c9e562012-05-17 11:01:02 -0700402 updateHideOrShowQuotedText(showQuotedText);
403 }
404
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700405 private boolean hadSavedInstanceStateMessage(Bundle savedInstanceState) {
406 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
407 }
408
Mindy Pereira71c9e562012-05-17 11:01:02 -0700409 private void updateHideOrShowQuotedText(boolean showQuotedText) {
410 mQuotedTextView.updateCheckedState(showQuotedText);
Mindy Pereira433b1982012-04-03 11:53:07 -0700411 }
412
413 private void setFocus(int action) {
414 if (action == EDIT_DRAFT) {
415 int type = mDraft.draftType;
416 switch (type) {
417 case UIProvider.DraftType.COMPOSE:
418 case UIProvider.DraftType.FORWARD:
419 action = COMPOSE;
420 break;
421 case UIProvider.DraftType.REPLY:
422 case UIProvider.DraftType.REPLY_ALL:
423 default:
424 action = REPLY;
425 break;
426 }
427 }
428 switch (action) {
429 case FORWARD:
430 case COMPOSE:
431 mTo.requestFocus();
432 break;
433 case REPLY:
434 case REPLY_ALL:
435 default:
436 focusBody();
437 break;
438 }
439 }
440
441 /**
442 * Focus the body of the message.
443 */
444 public void focusBody() {
445 mBodyView.requestFocus();
446 int length = mBodyView.getText().length();
447
448 int signatureStartPos = getSignatureStartPosition(
449 mSignature, mBodyView.getText().toString());
450 if (signatureStartPos > -1) {
451 // In case the user deleted the newlines...
452 mBodyView.setSelection(signatureStartPos);
453 } else if (length > 0) {
454 // Move cursor to the end.
455 mBodyView.setSelection(length);
456 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800457 }
458
459 @Override
460 protected void onResume() {
461 super.onResume();
462 // Update the from spinner as other accounts
463 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800464 if (mFromSpinner != null && mAccount != null) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700465 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount);
Mindy Pereira818143e2012-01-11 13:59:49 -0800466 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800467 }
468
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800469 @Override
470 protected void onPause() {
471 super.onPause();
472
473 if (mSendConfirmDialog != null) {
474 mSendConfirmDialog.dismiss();
475 }
476 if (mRecipientErrorDialog != null) {
477 mRecipientErrorDialog.dismiss();
478 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700479 // When the user exits the compose view, see if this draft needs saving.
480 if (isFinishing()) {
481 saveIfNeeded();
482 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800483 }
484
485 @Override
486 protected final void onActivityResult(int request, int result, Intent data) {
487 mAddingAttachment = false;
488
489 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
490 addAttachmentAndUpdateView(data);
491 }
492 }
493
494 @Override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700495 public final void onRestoreInstanceState(Bundle savedInstanceState) {
496 super.onRestoreInstanceState(savedInstanceState);
497 if (savedInstanceState != null) {
498 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
499 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
500 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
501 // There should be a focus and it should be an EditText since we
502 // only save these extras if these conditions are true.
503 EditText focusEditText = (EditText) getCurrentFocus();
504 final int length = focusEditText.getText().length();
505 if (selectionStart < length && selectionEnd < length) {
506 focusEditText.setSelection(selectionStart, selectionEnd);
507 }
508 }
509 }
510 }
511
512 @Override
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800513 public final void onSaveInstanceState(Bundle state) {
514 super.onSaveInstanceState(state);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700515 // The framework is happy to save and restore the selection but only if it also saves and
516 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
517 // this manually.
518 View focus = getCurrentFocus();
519 if (focus != null && focus instanceof EditText) {
520 EditText focusEditText = (EditText) focus;
521 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
522 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
523 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700524
525 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700526 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700527 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
528 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
529 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700530 if (selectedReplyFromAccount != null) {
531 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
532 .toString());
533 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
534 } else {
535 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
536 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800537
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700538 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
539 // We don't have a draft id, and we have a request id,
540 // save the request id.
541 state.putInt(EXTRA_REQUEST_ID, mRequestId);
542 }
543
544 // We want to restore the current mode after a pause
545 // or rotation.
546 int mode = getMode();
547 state.putInt(EXTRA_ACTION, mode);
548
549 Message message = createMessage(selectedReplyFromAccount, mode);
550 state.putParcelable(EXTRA_MESSAGE, message);
551
552 if (mRefMessage != null) {
553 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
554 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700555 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
556 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700557 }
558
559 private int getMode() {
560 int mode = ComposeActivity.COMPOSE;
561 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700562 if (actionBar != null
563 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700564 mode = actionBar.getSelectedNavigationIndex();
565 }
566 return mode;
567 }
568
569 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
570 Message message = new Message();
571 message.id = UIProvider.INVALID_MESSAGE_ID;
572 message.serverId =UIProvider.INVALID_MESSAGE_ID;
573 message.uri = null;
574 message.conversationUri = null;
575 message.subject = mSubject.getText().toString();
576 message.snippet = null;
Paul Westbrook91906812012-04-25 11:03:27 -0700577 message.from = selectedReplyFromAccount != null ?
578 selectedReplyFromAccount.name : mAccount.name;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700579 message.to = mTo.getText().toString();
Mindy Pereira4b1377e2012-04-18 15:08:05 -0700580 message.cc = mCc.getText().toString();
581 message.bcc = mBcc.getText().toString();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700582 message.replyTo = null;
583 message.dateReceivedMs = 0;
584 String htmlBody = Html.toHtml(mBodyView.getText());
585 StringBuilder fullBody = new StringBuilder(htmlBody);
586 message.bodyHtml = fullBody.toString();
587 message.bodyText = mBodyView.getText().toString();
588 message.embedsExternalResources = false;
589 message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
Mindy Pereirad2bef8b2012-05-30 12:14:52 -0700590 message.draftType = getDraftType(mode);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700591 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
592 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
593 message.hasAttachments = attachments != null && attachments.size() > 0;
594 message.attachmentListUri = null;
595 message.messageFlags = 0;
596 message.saveUri = null;
597 message.sendUri = null;
598 message.alwaysShowImages = false;
599 message.attachmentsJson = Attachment.toJSONArray(attachments);
600 CharSequence quotedText = mQuotedTextView.getQuotedText();
601 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
602 .getQuotedTextOffset(quotedText.toString()) : -1;
603 message.accountUri = null;
604 return message;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800605 }
606
Mindy Pereira818143e2012-01-11 13:59:49 -0800607 @VisibleForTesting
608 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700609 if (account == null) {
610 return;
611 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700612 if (!account.equals(mAccount)) {
613 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700614 mCachedSettings = mAccount.settings;
615 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700616 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800617 }
618
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700619 private void initFromSpinner(Bundle bundle, int action) {
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700620 String accountString = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700621 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700622 action = COMPOSE;
623 }
624 mFromSpinner.asyncInitFromSpinner(action, mAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700625 if (bundle != null) {
626 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
627 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
628 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
629 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
630 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
631 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
632 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700633 }
634 if (mReplyFromAccount == null) {
635 if (mDraft != null) {
636 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
637 } else if (mRefMessage != null) {
638 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
639 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700640 }
641 if (mReplyFromAccount == null) {
642 mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700643 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700644 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700645
Mindy Pereira62de1b12012-04-06 12:17:56 -0700646 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700647
Mindy Pereira62de1b12012-04-06 12:17:56 -0700648 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700649 // If there is only 1 account, just show that account.
650 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700651 // send mail from / save drafts to.
652 mFromStatic.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700653 mFromStaticText.setText(mAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700654 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700655 } else {
656 mFromStatic.setVisibility(View.VISIBLE);
657 mFromStaticText.setText(mAccount.name);
658 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700659 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800660 }
661
Mindy Pereira62de1b12012-04-06 12:17:56 -0700662 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
663 if (refMessage.accountUri != null) {
664 // This must be from combined inbox.
665 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
666 for (ReplyFromAccount from : replyFromAccounts) {
667 if (from.account.uri.equals(refMessage.accountUri)) {
668 return from;
669 }
670 }
671 return null;
672 } else {
673 return getReplyFromAccount(account, refMessage);
674 }
675 }
676
677 /**
678 * Given an account and which email address the message was sent to,
679 * return who the message should be sent from.
680 * @param account Account in which the message arrived.
681 * @param sentTo Email address to which the message was sent.
682 * @return the address from which to reply.
683 */
684 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
685 // First see if we are supposed to use the default address or
686 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -0700687 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700688 return getDefaultReplyFromAccount(account);
689 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -0700690 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -0700691 // all the message recipients and find one that matches
692 // a custom from or account.
693 List<String> allRecipients = new ArrayList<String>();
694 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.to)));
695 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.cc)));
696 return getMatchingRecipient(account, allRecipients);
697 }
698 }
699
700 /**
701 * Compare all the recipients of an email to the current account and all
702 * custom addresses associated with that account. Return the match if there
703 * is one, or the default account if there isn't.
704 */
705 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
706 // Tokenize the list and place in a hashmap.
707 ReplyFromAccount matchingReplyFrom = null;
708 Rfc822Token[] tokens;
709 HashSet<String> recipientsMap = new HashSet<String>();
710 for (String address : sentTo) {
711 tokens = Rfc822Tokenizer.tokenize(address);
712 for (int i = 0; i < tokens.length; i++) {
713 recipientsMap.add(tokens[i].getAddress());
714 }
715 }
716
717 int matchingAddressCount = 0;
718 List<ReplyFromAccount> customFroms;
719 try {
720 customFroms = FromAddressSpinner.getAccountSpecificFroms(account);
721 if (customFroms != null) {
722 for (ReplyFromAccount entry : customFroms) {
723 if (recipientsMap.contains(entry.address)) {
724 matchingReplyFrom = entry;
725 matchingAddressCount++;
726 }
727 }
728 }
729 } catch (JSONException e) {
730 LogUtils.wtf(LOG_TAG, "Exception parsing from addresses for account %s",
731 account.name);
732 }
733 if (matchingAddressCount > 1) {
734 matchingReplyFrom = getDefaultReplyFromAccount(account);
735 }
736 return matchingReplyFrom;
737 }
738
739 private ReplyFromAccount getDefaultReplyFromAccount(Account account) {
740 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
741 for (ReplyFromAccount from : replyFromAccounts) {
742 if (from.isDefault) {
743 return from;
744 }
745 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700746 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
747 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700748 }
749
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700750 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
751 String sender = msg.from;
Mindy Pereira62de1b12012-04-06 12:17:56 -0700752 ReplyFromAccount replyFromAccount = null;
753 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
754 if (TextUtils.equals(account.name, sender)) {
755 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700756 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700757 } else {
758 for (ReplyFromAccount fromAccount : replyFromAccounts) {
759 if (TextUtils.equals(fromAccount.name, sender)) {
760 replyFromAccount = fromAccount;
761 break;
762 }
763 }
764 }
765 return replyFromAccount;
766 }
767
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800768 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800769 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800770 if (mCcBccButton != null) {
771 mCcBccButton.setOnClickListener(this);
772 }
773 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800774 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800775 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
776 if (mAttachmentsButton != null) {
777 mAttachmentsButton.setOnClickListener(this);
778 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800779 mTo = (RecipientEditTextView) findViewById(R.id.to);
780 mCc = (RecipientEditTextView) findViewById(R.id.cc);
781 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800782 // TODO: add special chips text change watchers before adding
783 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800784 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800785 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
786 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -0700787 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800788 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800789 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800790 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800791 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800792 }
793
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700794 protected TextView getBody() {
795 return mBodyView;
796 }
797
798 @VisibleForTesting
799 public Account getFromAccount() {
800 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
801 mReplyFromAccount.account : mAccount;
802 }
803
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700804 private void clearChangeListeners() {
805 mSubject.removeTextChangedListener(this);
806 mBodyView.removeTextChangedListener(this);
807 mTo.removeTextChangedListener(mToListener);
808 mCc.removeTextChangedListener(mCcListener);
809 mBcc.removeTextChangedListener(mBccListener);
810 mFromSpinner.setOnAccountChangedListener(null);
811 mAttachmentsView.setAttachmentChangesListener(null);
812 }
813
Mindy Pereira75f66632012-01-11 11:42:02 -0800814 // Now that the message has been initialized from any existing draft or
815 // ref message data, set up listeners for any changes that occur to the
816 // message.
817 private void initChangeListeners() {
818 mSubject.addTextChangedListener(this);
819 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700820 if (mToListener == null) {
821 mToListener = new RecipientTextWatcher(mTo, this);
822 }
823 mTo.addTextChangedListener(mToListener);
824 if (mCcListener == null) {
825 mCcListener = new RecipientTextWatcher(mCc, this);
826 }
827 mCc.addTextChangedListener(mCcListener);
828 if (mBccListener == null) {
829 mBccListener = new RecipientTextWatcher(mBcc, this);
830 }
831 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -0800832 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800833 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800834 }
835
Mindy Pereira326c6602012-01-04 15:32:42 -0800836 private void initActionBar(int action) {
837 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800838 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700839 if (actionBar == null) {
840 return;
841 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800842 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800843 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
844 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800845 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800846 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800847 if (mComposeModeAdapter == null) {
848 mComposeModeAdapter = new ComposeModeAdapter(this);
849 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800850 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
851 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800852 switch (action) {
853 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800854 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800855 break;
856 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800857 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800858 break;
859 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800860 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800861 break;
862 }
863 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700864 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
865 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
866 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800867 }
868
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800869 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700870 setSubject(mRefMessage, action);
871 // Setup recipients
872 if (action == FORWARD) {
873 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800874 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700875 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700876 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700877 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
878 initAttachments(mRefMessage);
879 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800880 }
881
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700882 private void initFromDraftMessage(Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700883 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
884
885 mDraft = message;
886 mDraftId = message.id;
887 mSubject.setText(message.subject);
888 mForward = message.draftType == UIProvider.DraftType.FORWARD;
889 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
890 addToAddresses(toAddresses);
891 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
892 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -0700893 if (message.hasAttachments) {
894 List<Attachment> attachments = message.getAttachments();
895 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -0700896 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -0700897 }
898 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -0700899 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -0700900 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700901 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -0700902 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700903 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira002ff522012-05-30 10:31:26 -0700904 CharSequence htmlText = Html.fromHtml(message.bodyHtml);
905 if (quotedTextIndex > -1) {
906 htmlText = htmlText.subSequence(0, quotedTextIndex);
907 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
908 message.bodyHtml.length());
909 }
910 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700911 } else {
Mindy Pereira002ff522012-05-30 10:31:26 -0700912 CharSequence bodyText = quotedTextIndex > -1 ?
913 message.bodyText.substring(0, quotedTextIndex) : message.bodyText;
914 if (quotedTextIndex > -1) {
915 quotedText = message.bodyText.substring(quotedTextIndex);
916 }
917 mBodyView.setText(bodyText);
918 }
919 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -0700920 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700921 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700922 }
923
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700924 /**
925 * Fill all the widgets with the content found in the Intent Extra, if any.
926 * Also apply the same style to all widgets. Note: if initFromExtras is
927 * called as a result of switching between reply, reply all, and forward per
928 * the latest revision of Gmail, and the user has already made changes to
929 * attachments on a previous incarnation of the message (as a reply, reply
930 * all, or forward), the original attachments from the message will not be
931 * re-instantiated. The user's changes will be respected. This follows the
932 * web gmail interaction.
933 */
934 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700935 // If we were invoked with a SENDTO intent, the value
936 // should take precedence
937 final Uri dataUri = intent.getData();
938 if (dataUri != null) {
939 if (MAIL_TO.equals(dataUri.getScheme())) {
940 initFromMailTo(dataUri.toString());
941 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -0700942 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700943 String toText = dataUri.getSchemeSpecificPart();
944 if (toText != null) {
945 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -0700946 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700947 }
948 }
949 }
950 }
951
952 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
953 if (extraStrings != null) {
954 addToAddresses(Arrays.asList(extraStrings));
955 }
956 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
957 if (extraStrings != null) {
958 addCcAddresses(Arrays.asList(extraStrings), null);
959 }
960 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
961 if (extraStrings != null) {
962 addBccAddresses(Arrays.asList(extraStrings));
963 }
964
965 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
966 if (extraString != null) {
967 mSubject.setText(extraString);
968 }
969
970 for (String extra : ALL_EXTRAS) {
971 if (intent.hasExtra(extra)) {
972 String value = intent.getStringExtra(extra);
973 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700974 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700975 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700976 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700977 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700978 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700979 } else if (EXTRA_SUBJECT.equals(extra)) {
980 mSubject.setText(value);
981 } else if (EXTRA_BODY.equals(extra)) {
982 setBody(value, true /* with signature */);
983 }
984 }
985 }
986
987 Bundle extras = intent.getExtras();
988 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700989 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
990 if (text != null) {
991 setBody(text, true /* with signature */);
992 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700993 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700994 }
995
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700996 @VisibleForTesting
997 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -0700998 // TODO: handle the case where there are spaces in the display name as
999 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1000 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001001 // Since URLDecode.decode changes + into ' ', and + is a valid
1002 // email character, we need to find/ replace these ourselves before
1003 // decoding.
1004 String replacePlus = s.replace("+", "%2B");
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001005 try {
1006 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
1007 } catch (IllegalArgumentException e) {
1008 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1009 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1010 } else {
1011 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1012 }
1013 return null;
1014 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001015 }
1016
1017 /**
1018 * Initialize the compose view from a String representing a mailTo uri.
1019 * @param mailToString The uri as a string.
1020 */
1021 public void initFromMailTo(String mailToString) {
1022 // We need to disguise this string as a URI in order to parse it
1023 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1024 Uri uri = Uri.parse("foo://" + mailToString);
1025 int index = mailToString.indexOf("?");
1026 int length = "mailto".length() + 1;
1027 String to;
1028 try {
1029 // Extract the recipient after mailto:
1030 if (index == -1) {
1031 to = decodeEmailInUri(mailToString.substring(length));
1032 } else {
1033 to = decodeEmailInUri(mailToString.substring(length, index));
1034 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001035 if (!TextUtils.isEmpty(to)) {
1036 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1037 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001038 } catch (UnsupportedEncodingException e) {
1039 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1040 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1041 } else {
1042 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1043 }
1044 }
1045
1046 List<String> cc = uri.getQueryParameters("cc");
1047 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1048
1049 List<String> otherTo = uri.getQueryParameters("to");
1050 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1051
1052 List<String> bcc = uri.getQueryParameters("bcc");
1053 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1054
1055 List<String> subject = uri.getQueryParameters("subject");
1056 if (subject.size() > 0) {
1057 try {
1058 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
1059 } catch (UnsupportedEncodingException e) {
1060 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1061 e.getMessage(), subject);
1062 }
1063 }
1064
1065 List<String> body = uri.getQueryParameters("body");
1066 if (body.size() > 0) {
1067 try {
1068 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
1069 true /* with signature */);
1070 } catch (UnsupportedEncodingException e) {
1071 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1072 }
1073 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001074 }
1075
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001076 @VisibleForTesting
1077 protected void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -08001078 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001079 }
1080
Paul Westbrookf97588b2012-03-20 11:11:37 -07001081 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001082 Bundle extras = intent.getExtras();
1083 if (extras == null) {
1084 extras = Bundle.EMPTY;
1085 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001086 final String action = intent.getAction();
1087 if (!mAttachmentsChanged) {
1088 long totalSize = 0;
1089 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1090 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1091 for (String uriString : uris) {
1092 final Uri uri = Uri.parse(uriString);
1093 long size = 0;
1094 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001095 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001096 } catch (AttachmentFailureException e) {
1097 // A toast has already been shown to the user,
1098 // just break out of the loop.
1099 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1100 }
1101 totalSize += size;
1102 }
1103 }
1104 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
1105 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1106 long size = 0;
1107 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001108 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001109 } catch (AttachmentFailureException e) {
1110 // A toast has already been shown to the user, so just
1111 // exit.
1112 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1113 }
1114 totalSize += size;
1115 }
1116
1117 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
1118 && extras.containsKey(Intent.EXTRA_STREAM)) {
1119 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
1120 for (Parcelable uri : uris) {
1121 long size = 0;
1122 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001123 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001124 } catch (AttachmentFailureException e) {
1125 // A toast has already been shown to the user,
1126 // just break out of the loop.
1127 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1128 }
1129 totalSize += size;
1130 }
1131 }
1132
1133 if (totalSize > 0) {
1134 mAttachmentsChanged = true;
1135 updateSaveUi();
1136 }
1137 }
1138 }
1139
1140
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001141 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1142 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001143 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1144 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001145 }
1146
1147 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001148 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001149 boolean ccVisible = mCcBccView.isCcVisible();
1150 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001151 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001152 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001153 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001154 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001155 : R.string.add_bcc_label));
1156 } else {
1157 mCcBccButton.setVisibility(View.GONE);
1158 }
1159 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001160 }
1161
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001162 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001163 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1164 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1165 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1166 if (showCc || showBcc) {
1167 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001168 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001169 }
1170 }
1171
Mindy Pereira013194c2012-01-06 15:09:33 -08001172 /**
1173 * Add attachment and update the compose area appropriately.
1174 * @param data
1175 */
1176 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001177 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1178 }
1179
Andy Huang5c5fd572012-04-08 18:19:29 -07001180 public void addAttachmentAndUpdateView(Uri contentUri) {
1181 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001182 return;
1183 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001184 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001185 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1186 } catch (AttachmentFailureException e) {
1187 // A toast has already been shown to the user, no need to do
1188 // anything.
1189 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1190 }
1191 }
1192
1193 public void addAttachmentAndUpdateView(Attachment attachment) {
1194 try {
1195 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001196 if (size > 0) {
1197 mAttachmentsChanged = true;
1198 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001199 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001200 } catch (AttachmentFailureException e) {
1201 // A toast has already been shown to the user, no need to do
1202 // anything.
1203 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -08001204 }
1205 }
1206
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001207 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001208 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001209 // Don't populate the address if this is a forward.
1210 if (action == ComposeActivity.FORWARD) {
1211 return;
1212 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001213 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001214 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001215
Mindy Pereira818143e2012-01-11 13:59:49 -08001216 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001217 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001218 // This is the email address of the current user, i.e. the one composing
1219 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001220 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001221 String fromAddress = getAddress(refMessage.from);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001222 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
1223 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001224 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001225
1226 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1227 // Cc list is the union of the To and Cc recipients of the original
1228 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001229 // already on the To list.
1230 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001231 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001232 sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001233 addToAddresses(toAddresses);
1234 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001235 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001236 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1237 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -08001238 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001239 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001240 addRecipients(accountEmail, ccAddresses,
1241 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001242 addCcAddresses(ccAddresses, toAddresses);
1243 }
1244 }
1245
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001246 private String getAddress(String toParse) {
1247 if (!TextUtils.isEmpty(toParse)) {
1248 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(toParse);
1249 if (tokens.length > 0) {
1250 return tokens[0].getAddress();
1251 }
1252 }
1253 return "";
1254 }
1255
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001256 private void addToAddresses(Collection<String> addresses) {
1257 addAddressesToList(addresses, mTo);
1258 }
1259
1260 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001261 addCcAddressesToList(tokenizeAddressList(addresses),
1262 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001263 }
1264
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001265 private void addBccAddresses(Collection<String> addresses) {
1266 addAddressesToList(addresses, mBcc);
1267 }
1268
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001269 @VisibleForTesting
1270 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1271 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1272 String address;
1273
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001274 if (compareToList == null) {
1275 for (Rfc822Token[] tokens : addresses) {
1276 for (int i = 0; i < tokens.length; i++) {
1277 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001278 list.append(address + END_TOKEN);
1279 }
1280 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001281 } else {
1282 HashSet<String> compareTo = convertToHashSet(compareToList);
1283 for (Rfc822Token[] tokens : addresses) {
1284 for (int i = 0; i < tokens.length; i++) {
1285 address = tokens[i].toString();
1286 // Check if this is a duplicate:
1287 if (!compareTo.contains(tokens[i].getAddress())) {
1288 // Get the address here
1289 list.append(address + END_TOKEN);
1290 }
1291 }
1292 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001293 }
1294 }
1295
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001296 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
1297 HashSet<String> hash = new HashSet<String>();
1298 for (Rfc822Token[] tokens : list) {
1299 for (int i = 0; i < tokens.length; i++) {
1300 hash.add(tokens[i].getAddress());
1301 }
1302 }
1303 return hash;
1304 }
1305
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001306 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1307 @VisibleForTesting
1308 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1309
1310 for (String address: addresses) {
1311 tokenized.add(Rfc822Tokenizer.tokenize(address));
1312 }
1313 return tokenized;
1314 }
1315
1316 @VisibleForTesting
1317 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1318 for (String address : addresses) {
1319 addAddressToList(address, list);
1320 }
1321 }
1322
1323 private void addAddressToList(String address, RecipientEditTextView list) {
1324 if (address == null || list == null)
1325 return;
1326
1327 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1328
1329 for (int i = 0; i < tokens.length; i++) {
1330 list.append(tokens[i] + END_TOKEN);
1331 }
1332 }
1333
1334 @VisibleForTesting
1335 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001336 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001337 // The To recipient is the reply-to address specified in the original
1338 // message, unless it is:
1339 // the current user OR a custom from of the current user, in which case
1340 // it's the To recipient list of the original message.
1341 // OR missing, in which case use the sender of the original message
1342 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001343 if (!TextUtils.isEmpty(replyToAddress)) {
1344 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001345 } else {
Mindy Pereira1883b342012-06-20 08:34:56 -07001346 if (!TextUtils.equals(senderAddress, accountEmail)
1347 && !ReplyFromAccount.isCustomFrom(senderAddress,
1348 mFromSpinner.getReplyFromAccounts())) {
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001349 toAddresses.add(senderAddress);
1350 } else {
1351 // This happens if the user replies to a message they originally
Mindy Pereira1883b342012-06-20 08:34:56 -07001352 // wrote. In this case, "reply" really means "re-send," so we
1353 // target the original recipients. This works as expected even
1354 // if the user sent the original message to themselves.
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001355 toAddresses.addAll(Arrays.asList(inToAddresses));
1356 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001357 }
1358 return toAddresses;
1359 }
1360
1361 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
1362 for (String email : addresses) {
1363 // Do not add this account, or any of the custom froms, to the list
1364 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001365 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001366 if (!account.equalsIgnoreCase(recipientAddress)) {
1367 recipients.add(email.replace("\"\"", ""));
1368 }
1369 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001370 }
1371
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001372 private void setSubject(Message refMessage, int action) {
1373 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001374 String prefix;
1375 String correctedSubject = null;
1376 if (action == ComposeActivity.COMPOSE) {
1377 prefix = "";
1378 } else if (action == ComposeActivity.FORWARD) {
1379 prefix = getString(R.string.forward_subject_label);
1380 } else {
1381 prefix = getString(R.string.reply_subject_label);
1382 }
1383
1384 // Don't duplicate the prefix
1385 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
1386 correctedSubject = subject;
1387 } else {
1388 correctedSubject = String
1389 .format(getString(R.string.formatted_subject), prefix, subject);
1390 }
1391 mSubject.setText(correctedSubject);
1392 }
1393
Mindy Pereira818143e2012-01-11 13:59:49 -08001394 private void initRecipients() {
1395 setupRecipients(mTo);
1396 setupRecipients(mCc);
1397 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001398 }
1399
Mindy Pereira818143e2012-01-11 13:59:49 -08001400 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001401 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -08001402 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001403 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001404 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001405 int offset = accountName.indexOf("@") + 1;
1406 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001407 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001408 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001409 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001410 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001411 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001412 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001413 }
1414
1415 @Override
1416 public void onClick(View v) {
1417 int id = v.getId();
1418 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001419 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001420 // Verify that cc/ bcc aren't showing.
1421 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001422 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001423 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001424 case R.id.add_attachment:
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07001425 openAttachmentTypeSelectionDialog();
Mindy Pereira1f936682012-03-02 11:30:33 -08001426 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001427 }
1428 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001429
1430 @Override
1431 public boolean onCreateOptionsMenu(Menu menu) {
1432 super.onCreateOptionsMenu(menu);
1433 MenuInflater inflater = getMenuInflater();
1434 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001435 mSave = menu.findItem(R.id.save);
1436 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001437 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1438 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1439 if (helpItem != null) {
1440 helpItem.setVisible(mAccount != null
1441 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1442 }
1443 if (sendFeedbackItem != null) {
1444 sendFeedbackItem.setVisible(mAccount != null
1445 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1446 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001447 return true;
1448 }
1449
1450 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001451 public boolean onPrepareOptionsMenu(Menu menu) {
1452 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001453 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001454 // Its possible there is a menu item OR a button.
1455 boolean ccFieldVisible = mCc.isShown();
1456 boolean bccFieldVisible = mBcc.isShown();
1457 if (!ccFieldVisible || !bccFieldVisible) {
1458 ccBcc.setVisible(true);
1459 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1460 : R.string.add_bcc_label));
1461 } else {
1462 ccBcc.setVisible(false);
1463 }
1464 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001465 if (mSave != null) {
1466 mSave.setEnabled(shouldSave());
1467 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001468 return true;
1469 }
1470
1471 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001472 public boolean onOptionsItemSelected(MenuItem item) {
1473 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001474 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001475 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001476 case R.id.add_attachment:
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07001477 openAttachmentTypeSelectionDialog();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001478 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001479 case R.id.add_cc_bcc:
1480 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001481 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001482 case R.id.save:
Mindy Pereira48e31b02012-05-30 13:12:24 -07001483 doSave(true);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001484 break;
1485 case R.id.send:
1486 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001487 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001488 case R.id.discard:
1489 doDiscard();
1490 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001491 case R.id.settings:
1492 Utils.showSettings(this, mAccount);
1493 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001494 case android.R.id.home:
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001495 onAppUpPressed();
Mindy Pereirafbe40192012-03-20 10:40:45 -07001496 break;
1497 case R.id.help_info_menu_item:
1498 // TODO: enable context sensitive help
Paul Westbrook498e76d2012-04-12 16:33:02 -07001499 Utils.showHelp(this, mAccount, null);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001500 break;
1501 case R.id.feedback_menu_item:
1502 Utils.sendFeedback(this, mAccount);
1503 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001504 default:
1505 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001506 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001507 }
1508 return !handled ? super.onOptionsItemSelected(item) : handled;
1509 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001510
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001511 private void onAppUpPressed() {
1512 if (mLaunchedFromEmail) {
1513 // If this was started from Gmail, simply treat app up as the system back button, so
1514 // that the last view is restored.
1515 onBackPressed();
1516 return;
1517 }
1518
1519 // Fire the main activity to ensure it launches the "top" screen of mail.
1520 // Since the main Activity is singleTask, it should revive that task if it was already
1521 // started.
1522 final Intent mailIntent =
1523 Utils.createViewFolderIntent(mAccount.settings.defaultInbox, mAccount, null, false);
1524
1525 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
1526 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
1527 startActivity(mailIntent);
1528 finish();
1529 }
1530
Mindy Pereira33fe9082012-01-09 16:24:30 -08001531 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001532 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001533 }
1534
Mindy Pereira48e31b02012-05-30 13:12:24 -07001535 private void doSave(boolean showToast) {
1536 // Clear the IME composing suggestions from the body and subject before saving.
1537 clearImeText(mBodyView);
1538 clearImeText(mSubject);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001539 sendOrSaveWithSanityChecks(true, showToast, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07001540 }
1541
1542 private void clearImeText(TextView v) {
1543 v.clearComposingText();
1544 BaseInputConnection.removeComposingSpans(v.getEditableText());
Mindy Pereira33fe9082012-01-09 16:24:30 -08001545 }
1546
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001547 @VisibleForTesting
1548 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001549 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001550 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1551 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001552 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1553 }
1554
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001555 @VisibleForTesting
1556 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001557 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001558 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001559 public final SendOrSaveCallback mSendOrSaveCallback;
1560 @VisibleForTesting
1561 public final SendOrSaveMessage mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001562
1563 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1564 SendOrSaveCallback callback) {
1565 mContext = context;
1566 mSendOrSaveCallback = callback;
1567 mSendOrSaveMessage = message;
1568 }
1569
1570 @Override
1571 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001572 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001573
Mindy Pereira92551d02012-04-05 11:31:12 -07001574 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001575 Message message = mSendOrSaveCallback.getMessage();
1576 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001577 // If a previous draft has been saved, in an account that is different
1578 // than what the user wants to send from, remove the old draft, and treat this
1579 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001580 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001581 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1582 ContentResolver resolver = mContext.getContentResolver();
1583 ContentValues values = new ContentValues();
1584 values.put(BaseColumns._ID, messageId);
Mindy Pereira92551d02012-04-05 11:31:12 -07001585 if (selectedAccount.account.expungeMessageUri != null) {
1586 resolver.update(selectedAccount.account.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001587 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001588 } else {
1589 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001590 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001591 // reset messageId to 0, so a new message will be created
1592 messageId = UIProvider.INVALID_MESSAGE_ID;
1593 }
1594 }
1595
1596 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001597 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001598 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001599 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001600 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1601 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001602 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001603 ContentResolver resolver = mContext.getContentResolver();
Mindy Pereira92551d02012-04-05 11:31:12 -07001604 Uri messageUri = resolver
1605 .insert(sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
1606 : selectedAccount.account.sendMessageUri,
1607 sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001608 if (sendOrSaveMessage.mSave && messageUri != null) {
1609 Cursor messageCursor = resolver.query(messageUri,
1610 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001611 if (messageCursor != null) {
1612 try {
1613 if (messageCursor.moveToFirst()) {
1614 // Broadcast notification that a new message has
1615 // been allocated
1616 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1617 new Message(messageCursor));
1618 }
1619 } finally {
1620 messageCursor.close();
1621 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001622 }
1623 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001624 }
1625
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001626 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001627 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001628 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001629 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001630 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001631 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001632 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001633 }
1634 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1635 }
1636 }
1637
1638 // Array of the outstanding send or save tasks. Access is synchronized
1639 // with the object itself
1640 /* package for testing */
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001641 @VisibleForTesting
1642 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001643 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001644 private String mSignature;
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07001645 private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001646
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001647 @VisibleForTesting
1648 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07001649 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001650 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001651 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001652 @VisibleForTesting
1653 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001654 final int mRequestId;
1655
Mindy Pereira92551d02012-04-05 11:31:12 -07001656 public SendOrSaveMessage(ReplyFromAccount account, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001657 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001658 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001659 mValues = values;
1660 mRefMessageId = refMessageId;
1661 mSave = save;
1662 mRequestId = mValues.hashCode() ^ hashCode();
1663 }
1664
1665 int requestId() {
1666 return mRequestId;
1667 }
1668 }
1669
1670 /**
1671 * Get the to recipients.
1672 */
1673 public String[] getToAddresses() {
1674 return getAddressesFromList(mTo);
1675 }
1676
1677 /**
1678 * Get the cc recipients.
1679 */
1680 public String[] getCcAddresses() {
1681 return getAddressesFromList(mCc);
1682 }
1683
1684 /**
1685 * Get the bcc recipients.
1686 */
1687 public String[] getBccAddresses() {
1688 return getAddressesFromList(mBcc);
1689 }
1690
1691 public String[] getAddressesFromList(RecipientEditTextView list) {
1692 if (list == null) {
1693 return new String[0];
1694 }
1695 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1696 int count = tokens.length;
1697 String[] result = new String[count];
1698 for (int i = 0; i < count; i++) {
1699 result[i] = tokens[i].toString();
1700 }
1701 return result;
1702 }
1703
1704 /**
1705 * Check for invalid email addresses.
1706 * @param to String array of email addresses to check.
1707 * @param wrongEmailsOut Emails addresses that were invalid.
1708 */
1709 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07001710 if (mValidator == null) {
1711 return;
1712 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001713 for (String email : to) {
1714 if (!mValidator.isValid(email)) {
1715 wrongEmailsOut.add(email);
1716 }
1717 }
1718 }
1719
1720 /**
1721 * Show an error because the user has entered an invalid recipient.
1722 * @param message
1723 */
1724 public void showRecipientErrorDialog(String message) {
1725 // Only 1 invalid recipients error dialog should be allowed up at a
1726 // time.
1727 if (mRecipientErrorDialog != null) {
1728 mRecipientErrorDialog.dismiss();
1729 }
1730 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1731 R.string.recipient_error_dialog_title)
1732 .setIconAttribute(android.R.attr.alertDialogIcon)
Mindy Pereira82cc5662012-01-09 17:29:30 -08001733 .setPositiveButton(
1734 R.string.ok, new Dialog.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001735 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08001736 public void onClick(DialogInterface dialog, int which) {
1737 // after the user dismisses the recipient error
1738 // dialog we want to make sure to refocus the
1739 // recipient to field so they can fix the issue
1740 // easily
1741 if (mTo != null) {
1742 mTo.requestFocus();
1743 }
1744 mRecipientErrorDialog = null;
1745 }
1746 }).show();
1747 }
1748
1749 /**
1750 * Update the state of the UI based on whether or not the current draft
1751 * needs to be saved and the message is not empty.
1752 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001753 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001754 if (mSave != null) {
1755 mSave.setEnabled((shouldSave() && !isBlank()));
1756 }
1757 }
1758
1759 /**
1760 * Returns true if we need to save the current draft.
1761 */
1762 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001763 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001764 // The message should only be saved if:
1765 // It hasn't been sent AND
1766 // Some text has been added to the message OR
1767 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07001768 // AND there is actually something in the draft to save.
1769 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
1770 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001771 }
1772 }
1773
1774 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001775 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001776 * @return boolean
1777 */
1778 public boolean isBlank() {
1779 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001780 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1781 mBodyView.getText().toString()) == 0)
1782 && mTo.length() == 0
1783 && mCc.length() == 0 && mBcc.length() == 0
1784 && mAttachmentsView.getAttachments().size() == 0;
1785 }
1786
1787 @VisibleForTesting
1788 protected int getSignatureStartPosition(String signature, String bodyText) {
1789 int startPos = -1;
1790
1791 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1792 return startPos;
1793 }
1794
1795 int bodyLength = bodyText.length();
1796 int signatureLength = signature.length();
1797 String printableVersion = convertToPrintableSignature(signature);
1798 int printableLength = printableVersion.length();
1799
1800 if (bodyLength >= printableLength
1801 && bodyText.substring(bodyLength - printableLength)
1802 .equals(printableVersion)) {
1803 startPos = bodyLength - printableLength;
1804 } else if (bodyLength >= signatureLength
1805 && bodyText.substring(bodyLength - signatureLength)
1806 .equals(signature)) {
1807 startPos = bodyLength - signatureLength;
1808 }
1809 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001810 }
1811
1812 /**
1813 * Allows any changes made by the user to be ignored. Called when the user
1814 * decides to discard a draft.
1815 */
1816 private void discardChanges() {
1817 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001818 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001819 mReplyFromChanged = false;
1820 }
1821
1822 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001823 * @param body
1824 * @param save
1825 * @param showToast
1826 * @return Whether the send or save succeeded.
1827 */
1828 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1829 final boolean orientationChanged) {
1830 String[] to, cc, bcc;
1831 Editable body = mBodyView.getEditableText();
Mindy Pereira181df782012-03-01 13:32:44 -08001832 if (orientationChanged) {
1833 to = cc = bcc = new String[0];
1834 } else {
1835 to = getToAddresses();
1836 cc = getCcAddresses();
1837 bcc = getBccAddresses();
1838 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001839
Mindy Pereira181df782012-03-01 13:32:44 -08001840 // Don't let the user send to nobody (but it's okay to save a message
1841 // with no recipients)
1842 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1843 showRecipientErrorDialog(getString(R.string.recipient_needed));
1844 return false;
1845 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001846
Mindy Pereira181df782012-03-01 13:32:44 -08001847 List<String> wrongEmails = new ArrayList<String>();
1848 if (!save) {
1849 checkInvalidEmails(to, wrongEmails);
1850 checkInvalidEmails(cc, wrongEmails);
1851 checkInvalidEmails(bcc, wrongEmails);
1852 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001853
Mindy Pereira181df782012-03-01 13:32:44 -08001854 // Don't let the user send an email with invalid recipients
1855 if (wrongEmails.size() > 0) {
1856 String errorText = String.format(getString(R.string.invalid_recipient),
1857 wrongEmails.get(0));
1858 showRecipientErrorDialog(errorText);
1859 return false;
1860 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001861
Mindy Pereira181df782012-03-01 13:32:44 -08001862 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001863 @Override
Mindy Pereira181df782012-03-01 13:32:44 -08001864 public void onClick(DialogInterface dialog, int which) {
1865 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1866 }
1867 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001868
Mindy Pereira181df782012-03-01 13:32:44 -08001869 // Show a warning before sending only if there are no attachments.
1870 if (!save) {
1871 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1872 boolean warnAboutEmptySubject = isSubjectEmpty();
1873 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001874
Mindy Pereira181df782012-03-01 13:32:44 -08001875 // A warning about an empty body may not be warranted when
1876 // forwarding mails, since a common use case is to forward
1877 // quoted text and not append any more text.
1878 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001879
Mindy Pereira181df782012-03-01 13:32:44 -08001880 // When we bring up a dialog warning the user about a send,
1881 // assume that they accept sending the message. If they do not,
1882 // the dialog listener is required to enable sending again.
1883 if (warnAboutEmptySubject) {
1884 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1885 return true;
1886 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001887
Mindy Pereira181df782012-03-01 13:32:44 -08001888 if (warnAboutEmptyBody) {
1889 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1890 return true;
1891 }
1892 }
1893 // Ask for confirmation to send (if always required)
1894 if (showSendConfirmation()) {
1895 showSendConfirmDialog(R.string.confirm_send_message, listener);
1896 return true;
1897 }
1898 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001899
Mindy Pereira181df782012-03-01 13:32:44 -08001900 sendOrSave(body, save, showToast, false);
1901 return true;
1902 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001903
Mindy Pereira181df782012-03-01 13:32:44 -08001904 /**
1905 * Returns a boolean indicating whether warnings should be shown for empty
1906 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07001907 *
Mindy Pereira181df782012-03-01 13:32:44 -08001908 * @return True if a warning should be shown for empty text fields
1909 */
1910 protected boolean showEmptyTextWarnings() {
1911 return mAttachmentsView.getAttachments().size() == 0;
1912 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001913
Mindy Pereira181df782012-03-01 13:32:44 -08001914 /**
1915 * Returns a boolean indicating whether the user should confirm each send
1916 *
1917 * @return True if a warning should be on each send
1918 */
1919 protected boolean showSendConfirmation() {
1920 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1921 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001922
Mindy Pereira181df782012-03-01 13:32:44 -08001923 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1924 if (mSendConfirmDialog != null) {
1925 mSendConfirmDialog.dismiss();
1926 mSendConfirmDialog = null;
1927 }
1928 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1929 .setTitle(R.string.confirm_send_title)
1930 .setIconAttribute(android.R.attr.alertDialogIcon)
1931 .setPositiveButton(R.string.send, listener)
Mindy Pereira6edd5972012-06-19 10:22:36 -07001932 .setNegativeButton(R.string.cancel, this)
1933 .show();
Mindy Pereira181df782012-03-01 13:32:44 -08001934 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001935
Mindy Pereira181df782012-03-01 13:32:44 -08001936 /**
1937 * Returns whether the ComposeArea believes there is any text in the body of
1938 * the composition. TODO: When ComposeArea controls the Body as well, add
1939 * that here.
1940 */
1941 public boolean isBodyEmpty() {
1942 return !mQuotedTextView.isTextIncluded();
1943 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001944
Mindy Pereira181df782012-03-01 13:32:44 -08001945 /**
1946 * Test to see if the subject is empty.
1947 *
1948 * @return boolean.
1949 */
1950 // TODO: this will likely go away when composeArea.focus() is implemented
1951 // after all the widget control is moved over.
1952 public boolean isSubjectEmpty() {
1953 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1954 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001955
Mindy Pereira181df782012-03-01 13:32:44 -08001956 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001957 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07001958 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
1959 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001960 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001961
Mindy Pereirac2031972012-04-03 09:38:35 -07001962 String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
1963
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001964 MessageModification.putToAddresses(values, message.getToAddresses());
1965 MessageModification.putCcAddresses(values, message.getCcAddresses());
1966 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001967
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001968 MessageModification.putCustomFromAddress(values, message.from);
Mindy Pereira92551d02012-04-05 11:31:12 -07001969
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001970 MessageModification.putSubject(values, message.subject);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001971 String htmlBody = Html.toHtml(body);
Paul Westbrook05b92b82012-04-20 13:29:37 -07001972
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001973 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1974 StringBuilder fullBody = new StringBuilder(htmlBody);
1975 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001976 // HTML gets converted to text for now
1977 final String text = quotedText.toString();
1978 if (QuotedTextView.containsQuotedText(text)) {
1979 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07001980 final int quoteStartPos = fullBody.length() + pos;
1981 fullBody.append(text);
1982 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07001983 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07001984 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001985 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001986 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1987 // This shouldn't happen, but just use what we have,
1988 // and don't do server-side expansion
1989 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001990 }
1991 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001992 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07001993 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07001994 if (refMessage != null) {
1995 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1996 MessageModification.putBodyHtml(values, fullBody.toString());
1997 }
1998 if (!TextUtils.isEmpty(refMessage.bodyText)) {
1999 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
2000 }
2001 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002002 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002003 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
2004 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002005 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002006 if (!TextUtils.isEmpty(refMessageId)) {
2007 MessageModification.putRefMessageId(values, refMessageId);
2008 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002009
Mindy Pereira92551d02012-04-05 11:31:12 -07002010 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(replyFromAccount,
Mindy Pereira181df782012-03-01 13:32:44 -08002011 values, refMessageId, save);
2012 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002013
Mindy Pereira181df782012-03-01 13:32:44 -08002014 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002015
Mindy Pereira181df782012-03-01 13:32:44 -08002016 // Do the send/save action on the specified handler to avoid possible
2017 // ANRs
2018 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002019
Mindy Pereira181df782012-03-01 13:32:44 -08002020 return sendOrSaveMessage.requestId();
2021 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002022
Mindy Pereira002ff522012-05-30 10:31:26 -07002023 private static int getDraftType(int mode) {
2024 int draftType = -1;
2025 switch (mode) {
2026 case ComposeActivity.COMPOSE:
2027 draftType = DraftType.COMPOSE;
2028 break;
2029 case ComposeActivity.REPLY:
2030 draftType = DraftType.REPLY;
2031 break;
2032 case ComposeActivity.REPLY_ALL:
2033 draftType = DraftType.REPLY_ALL;
2034 break;
2035 case ComposeActivity.FORWARD:
2036 draftType = DraftType.FORWARD;
2037 break;
2038 }
2039 return draftType;
2040 }
2041
Mindy Pereira181df782012-03-01 13:32:44 -08002042 private void sendOrSave(Spanned body, boolean save, boolean showToast,
2043 boolean orientationChanged) {
2044 // Check if user is a monkey. Monkeys can compose and hit send
2045 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002046 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002047 return;
2048 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002049
Mindy Pereira181df782012-03-01 13:32:44 -08002050 String[] to, cc, bcc;
2051 if (orientationChanged) {
2052 to = cc = bcc = new String[0];
2053 } else {
2054 to = getToAddresses();
2055 cc = getCcAddresses();
2056 bcc = getBccAddresses();
2057 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002058
Mindy Pereira181df782012-03-01 13:32:44 -08002059 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002060 private int mRestoredRequestId;
2061
Marc Blank0bbc8582012-04-23 15:07:57 -07002062 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002063 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002064 synchronized (mActiveTasks) {
2065 int numTasks = mActiveTasks.size();
2066 if (numTasks == 0) {
2067 // Start service so we won't be killed if this app is
2068 // put in the background.
2069 startService(new Intent(ComposeActivity.this, EmptyService.class));
2070 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002071
Mindy Pereira181df782012-03-01 13:32:44 -08002072 mActiveTasks.add(sendOrSaveTask);
2073 }
2074 if (sTestSendOrSaveCallback != null) {
2075 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2076 }
2077 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002078
Marc Blank0bbc8582012-04-23 15:07:57 -07002079 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002080 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2081 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002082 synchronized (mDraftLock) {
2083 mDraftId = message.id;
2084 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002085 if (sRequestMessageIdMap != null) {
2086 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2087 }
Mindy Pereira181df782012-03-01 13:32:44 -08002088 // Cache request message map, in case the process is killed
2089 saveRequestMap();
2090 }
2091 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002092 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002093 }
2094 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002095
Marc Blank0bbc8582012-04-23 15:07:57 -07002096 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002097 public Message getMessage() {
2098 synchronized (mDraftLock) {
2099 return mDraft;
2100 }
2101 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002102
Marc Blank0bbc8582012-04-23 15:07:57 -07002103 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002104 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
2105 if (success) {
2106 // Successfully sent or saved so reset change markers
2107 discardChanges();
2108 } else {
2109 // A failure happened with saving/sending the draft
2110 // TODO(pwestbro): add a better string that should be used
2111 // when failing to send or save
2112 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2113 .show();
2114 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002115
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002116 int numTasks;
2117 synchronized (mActiveTasks) {
2118 // Remove the task from the list of active tasks
2119 mActiveTasks.remove(task);
2120 numTasks = mActiveTasks.size();
2121 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002122
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002123 if (numTasks == 0) {
2124 // Stop service so we can be killed.
2125 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2126 }
2127 if (sTestSendOrSaveCallback != null) {
2128 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2129 }
2130 }
Mindy Pereira181df782012-03-01 13:32:44 -08002131 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002132
Mindy Pereira181df782012-03-01 13:32:44 -08002133 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07002134 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002135 String fromAddress = selectedAccount.name;
2136 if (selectedAccount == null || fromAddress == null) {
2137 // We don't have either the selected account or from address,
2138 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07002139 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002140 fromAddress = mAccount.name;
2141 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002142
Mindy Pereira181df782012-03-01 13:32:44 -08002143 if (mSendSaveTaskHandler == null) {
2144 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2145 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002146
Mindy Pereira181df782012-03-01 13:32:44 -08002147 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2148 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002149
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002150 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002151 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2152 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Mindy Pereira12575862012-03-21 16:30:54 -07002153 mSendSaveTaskHandler, save, mComposeMode);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002154
Mindy Pereira181df782012-03-01 13:32:44 -08002155 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
2156 mRecipient = selectedAccount.name;
2157 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002158 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002159
Mindy Pereira181df782012-03-01 13:32:44 -08002160 // Don't display the toast if the user is just changing the orientation,
2161 // but we still need to save the draft to the cursor because this is how we restore
2162 // the attachments when the configuration change completes.
2163 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2164 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2165 Toast.LENGTH_LONG).show();
2166 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002167
Mindy Pereira181df782012-03-01 13:32:44 -08002168 // Need to update variables here because the send or save completes
2169 // asynchronously even though the toast shows right away.
2170 discardChanges();
2171 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002172
Mindy Pereira181df782012-03-01 13:32:44 -08002173 // If we are sending, finish the activity
2174 if (!save) {
2175 finish();
2176 }
2177 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002178
Mindy Pereira181df782012-03-01 13:32:44 -08002179 /**
2180 * Save the state of the request messageid map. This allows for the Gmail
2181 * process to be killed, but and still allow for ComposeActivity instances
2182 * to be recreated correctly.
2183 */
2184 private void saveRequestMap() {
2185 // TODO: store the request map in user preferences.
2186 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002187
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07002188 public void openAttachmentTypeSelectionDialog() {
2189 AlertDialog.Builder builder = new AlertDialog.Builder(this);
2190 builder.setTitle(R.string.add_file_attachment);
2191 builder.setAdapter(new AttachmentTypeSelectorAdapter(this),
2192 new DialogInterface.OnClickListener() {
2193 public void onClick(DialogInterface dialog, int position) {
2194 doAttach(position);
2195 }
2196 });
2197 builder.show();
2198 }
2199
2200 private void doAttach(int position) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002201 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2202 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002203 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07002204 i.setType(AttachmentTypeSelectorAdapter.ITEMS.get(position).mMimeType);
Mindy Pereira013194c2012-01-06 15:09:33 -08002205 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002206 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2207 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002208 }
2209
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002210 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002211 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002212 if (mCcBccButton != null) {
2213 mCcBccButton.setVisibility(View.GONE);
2214 }
2215 }
2216
Mindy Pereira326c6602012-01-04 15:32:42 -08002217 @Override
2218 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002219 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002220 if (position == ComposeActivity.REPLY) {
2221 mComposeMode = ComposeActivity.REPLY;
2222 } else if (position == ComposeActivity.REPLY_ALL) {
2223 mComposeMode = ComposeActivity.REPLY_ALL;
2224 } else if (position == ComposeActivity.FORWARD) {
2225 mComposeMode = ComposeActivity.FORWARD;
2226 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002227 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002228 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002229 resetMessageForModeChange();
Mindy Pereiraef388302012-06-18 19:07:44 -07002230 if (mDraft == null && mRefMessage != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002231 initFromRefMessage(mComposeMode, mAccount.name);
2232 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002233 boolean showCc = false;
2234 boolean showBcc = false;
2235 if (mDraft != null) {
2236 // Following desktop behavior, if the user has added a BCC
2237 // field to a draft, we show it regardless of compose mode.
2238 showBcc = !TextUtils.isEmpty(mDraft.bcc);
2239 // Use the draft to determine what to populate.
2240 // If the Bcc field is showing, show the Cc field whether it is populated or not.
2241 showCc = showBcc || (!TextUtils.isEmpty(mDraft.cc) && mComposeMode == REPLY_ALL);
2242 } else if (mRefMessage != null) {
2243 showCc = mComposeMode == REPLY_ALL && !TextUtils.isEmpty(mRefMessage.cc);
2244 }
2245 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002246 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002247 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002248 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08002249 return true;
2250 }
2251
Mindy Pereirab3112a22012-06-20 12:10:03 -07002252 @VisibleForTesting
2253 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08002254 // When switching between reply, reply all, forward,
2255 // follow the behavior of webview.
2256 // The contents of the following fields are cleared
2257 // so that they can be populated directly from the
2258 // ref message:
2259 // 1) Any recipient fields
2260 // 2) The subject
2261 mTo.setText("");
2262 mCc.setText("");
2263 mBcc.setText("");
2264 // Any edits to the subject are replaced with the original subject.
2265 mSubject.setText("");
2266
2267 // Any changes to the contents of the following fields are kept:
2268 // 1) Body
2269 // 2) Attachments
2270 // If the user made changes to attachments, keep their changes.
2271 if (!mAttachmentsChanged) {
2272 mAttachmentsView.deleteAllAttachments();
2273 }
2274 }
2275
Mindy Pereira326c6602012-01-04 15:32:42 -08002276 private class ComposeModeAdapter extends ArrayAdapter<String> {
2277
2278 private LayoutInflater mInflater;
2279
2280 public ComposeModeAdapter(Context context) {
2281 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2282 .getStringArray(R.array.compose_modes));
2283 }
2284
2285 private LayoutInflater getInflater() {
2286 if (mInflater == null) {
2287 mInflater = LayoutInflater.from(getContext());
2288 }
2289 return mInflater;
2290 }
2291
2292 @Override
2293 public View getView(int position, View convertView, ViewGroup parent) {
2294 if (convertView == null) {
2295 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2296 }
2297 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2298 return super.getView(position, convertView, parent);
2299 }
2300 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002301
2302 @Override
2303 public void onRespondInline(String text) {
2304 appendToBody(text, false);
2305 }
2306
2307 /**
2308 * Append text to the body of the message. If there is no existing body
2309 * text, just sets the body to text.
2310 *
2311 * @param text
2312 * @param withSignature True to append a signature.
2313 */
2314 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002315 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002316 if (bodyText != null && bodyText.length() > 0) {
2317 bodyText.append(text);
2318 } else {
2319 setBody(text, withSignature);
2320 }
2321 }
2322
2323 /**
2324 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002325 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002326 * @param text
2327 * @param withSignature True to append a signature.
2328 */
2329 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002330 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002331 if (withSignature) {
2332 appendSignature();
2333 }
2334 }
2335
2336 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002337 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002338 boolean hasFocus = mBodyView.hasFocus();
Mindy Pereirab13917c2012-03-29 08:08:19 -07002339 if (!TextUtils.equals(newSignature, mSignature)) {
2340 mSignature = newSignature;
2341 if (!TextUtils.isEmpty(mSignature)
2342 && getSignatureStartPosition(mSignature,
2343 mBodyView.getText().toString()) < 0) {
2344 // Appending a signature does not count as changing text.
2345 mBodyView.removeTextChangedListener(this);
2346 mBodyView.append(convertToPrintableSignature(mSignature));
2347 mBodyView.addTextChangedListener(this);
2348 }
Mindy Pereira433b1982012-04-03 11:53:07 -07002349 if (hasFocus) {
2350 focusBody();
2351 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002352 }
2353 }
2354
2355 private String convertToPrintableSignature(String signature) {
2356 String signatureResource = getResources().getString(R.string.signature);
2357 if (signature == null) {
2358 signature = "";
2359 }
2360 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002361 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002362
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08002363 @Override
2364 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07002365 mReplyFromAccount = mFromSpinner.getCurrentAccount();
2366 if (!mAccount.equals(mReplyFromAccount.account)) {
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002367 setAccount(mReplyFromAccount.account);
2368
Mindy Pereira181df782012-03-01 13:32:44 -08002369 // TODO: handle discarding attachments when switching accounts.
2370 // Only enable save for this draft if there is any other content
2371 // in the message.
2372 if (!isBlank()) {
2373 enableSave(true);
2374 }
2375 mReplyFromChanged = true;
2376 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002377 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002378 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002379
2380 public void enableSave(boolean enabled) {
2381 if (mSave != null) {
2382 mSave.setEnabled(enabled);
2383 }
2384 }
2385
2386 public void enableSend(boolean enabled) {
2387 if (mSend != null) {
2388 mSend.setEnabled(enabled);
2389 }
2390 }
2391
2392 /**
2393 * Handles button clicks from any error dialogs dealing with sending
2394 * a message.
2395 */
2396 @Override
2397 public void onClick(DialogInterface dialog, int which) {
2398 switch (which) {
2399 case DialogInterface.BUTTON_POSITIVE: {
2400 doDiscardWithoutConfirmation(true /* show toast */ );
2401 break;
2402 }
2403 case DialogInterface.BUTTON_NEGATIVE: {
2404 // If the user cancels the send, re-enable the send button.
2405 enableSend(true);
2406 break;
2407 }
2408 }
2409
2410 }
2411
Mindy Pereiraefe3d252012-03-01 14:20:44 -08002412 private void doDiscard() {
2413 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2414 .setPositiveButton(R.string.ok, this)
2415 .setNegativeButton(R.string.cancel, null)
2416 .create().show();
2417 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002418 /**
2419 * Effectively discard the current message.
2420 *
2421 * This method is either invoked from the menu or from the dialog
2422 * once the user has confirmed that they want to discard the message.
2423 * @param showToast show "Message discarded" toast if true
2424 */
2425 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002426 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002427 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2428 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07002429 values.put(BaseColumns._ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002430 if (mAccount.expungeMessageUri != null) {
2431 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2432 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07002433 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002434 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002435 // This is not strictly necessary (since we should not try to
2436 // save the draft after calling this) but it ensures that if we
2437 // do save again for some reason we make a new draft rather than
2438 // trying to resave an expunged draft.
2439 mDraftId = UIProvider.INVALID_MESSAGE_ID;
2440 }
2441 }
2442
2443 if (showToast) {
2444 // Display a toast to let the user know
2445 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2446 }
2447
2448 // This prevents the draft from being saved in onPause().
2449 discardChanges();
2450 finish();
2451 }
2452
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002453 private void saveIfNeeded() {
2454 if (mAccount == null) {
2455 // We have not chosen an account yet so there's no way that we can save. This is ok,
2456 // though, since we are saving our state before AccountsActivity is activated. Thus, the
2457 // user has not interacted with us yet and there is no real state to save.
2458 return;
2459 }
2460
2461 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07002462 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002463 }
2464 }
2465
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002466 @Override
2467 public void onAttachmentDeleted() {
2468 mAttachmentsChanged = true;
2469 updateSaveUi();
2470 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002471
2472
2473 /**
2474 * This is called any time one of our text fields changes.
2475 */
Marc Blank0bbc8582012-04-23 15:07:57 -07002476 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002477 public void afterTextChanged(Editable s) {
2478 mTextChanged = true;
2479 updateSaveUi();
2480 }
2481
2482 @Override
2483 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2484 // Do nothing.
2485 }
2486
Marc Blank0bbc8582012-04-23 15:07:57 -07002487 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002488 public void onTextChanged(CharSequence s, int start, int before, int count) {
2489 // Do nothing.
2490 }
2491
2492
2493 // There is a big difference between the text associated with an address changing
2494 // to add the display name or to format properly and a recipient being added or deleted.
2495 // Make sure we only notify of changes when a recipient has been added or deleted.
2496 private class RecipientTextWatcher implements TextWatcher {
2497 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2498
2499 private RecipientEditTextView mView;
2500
2501 private TextWatcher mListener;
2502
2503 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2504 mView = view;
2505 mListener = listener;
2506 }
2507
2508 @Override
2509 public void afterTextChanged(Editable s) {
2510 if (hasChanged()) {
2511 mListener.afterTextChanged(s);
2512 }
2513 }
2514
2515 private boolean hasChanged() {
2516 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2517 int totalCount = currRecips.length;
2518 int totalPrevCount = 0;
2519 for (Entry<String, Integer> entry : mContent.entrySet()) {
2520 totalPrevCount += entry.getValue();
2521 }
2522 if (totalCount != totalPrevCount) {
2523 return true;
2524 }
2525
2526 for (String recip : currRecips) {
2527 if (!mContent.containsKey(recip)) {
2528 return true;
2529 } else {
2530 int count = mContent.get(recip) - 1;
2531 if (count < 0) {
2532 return true;
2533 } else {
2534 mContent.put(recip, count);
2535 }
2536 }
2537 }
2538 return false;
2539 }
2540
2541 private String[] tokenizeRecips(String[] recips) {
2542 // Tokenize them all and put them in the list.
2543 String[] recipAddresses = new String[recips.length];
2544 for (int i = 0; i < recips.length; i++) {
2545 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2546 }
2547 return recipAddresses;
2548 }
2549
2550 @Override
2551 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2552 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2553 for (String recip : recips) {
2554 if (!mContent.containsKey(recip)) {
2555 mContent.put(recip, 1);
2556 } else {
2557 mContent.put(recip, (mContent.get(recip)) + 1);
2558 }
2559 }
2560 }
2561
2562 @Override
2563 public void onTextChanged(CharSequence s, int start, int before, int count) {
2564 // Do nothing.
2565 }
2566 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002567
2568 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
2569 if (sTestSendOrSaveCallback != null && testCallback != null) {
2570 throw new IllegalStateException("Attempting to register more than one test callback");
2571 }
2572 sTestSendOrSaveCallback = testCallback;
2573 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07002574
2575 @VisibleForTesting
2576 protected ArrayList<Attachment> getAttachments() {
2577 return mAttachmentsView.getAttachments();
2578 }
Vikram Aggarwal8183d452012-04-17 09:13:29 -07002579}