blob: ecce59ed4e0e24797debc5048e02b04e37e94476 [file] [log] [blame]
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Mindy Pereira326c6602012-01-04 15:32:42 -080019import android.app.ActionBar;
Mindy Pereira82cc5662012-01-09 17:29:30 -080020import android.app.ActivityManager;
21import android.app.AlertDialog;
22import android.app.Dialog;
Mindy Pereira326c6602012-01-04 15:32:42 -080023import android.app.ActionBar.OnNavigationListener;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080024import android.app.Activity;
Mindy 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 Pereira6349a042012-01-04 11:25:01 -080031import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080032import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080033import android.os.Handler;
34import android.os.HandlerThread;
Mindy Pereira82cc5662012-01-09 17:29:30 -080035import android.provider.BaseColumns;
Mindy Pereira013194c2012-01-06 15:09:33 -080036import android.provider.Settings;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080037import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080038import android.text.Html;
39import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080040import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080041import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080042import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080043import android.text.util.Rfc822Tokenizer;
Mindy Pereira326c6602012-01-04 15:32:42 -080044import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080045import android.view.Menu;
46import android.view.MenuInflater;
47import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080048import android.view.View;
Mindy Pereira326c6602012-01-04 15:32:42 -080049import android.view.ViewGroup;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080050import android.view.View.OnClickListener;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080051import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080052import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080053import android.widget.Button;
Mindy Pereira6349a042012-01-04 11:25:01 -080054import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080055import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080056
Mindy Pereirac17d0732011-12-29 10:46:19 -080057import com.android.common.Rfc822Validator;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080058import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080059import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080060import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080061import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080062import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080063import com.android.mail.providers.Address;
64import com.android.mail.providers.Attachment;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080065import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080066import com.android.mail.providers.MessageModification;
Andy Huang30e2c242012-01-06 18:14:30 -080067import com.android.mail.providers.UIProvider;
Mindy Pereira82cc5662012-01-09 17:29:30 -080068import com.android.mail.providers.UIProvider.MessageColumns;
Andy Huang30e2c242012-01-06 18:14:30 -080069import com.android.mail.R;
Andy Huang30e2c242012-01-06 18:14:30 -080070import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080071import com.android.mail.utils.Utils;
Mindy Pereirac17d0732011-12-29 10:46:19 -080072import com.android.ex.chips.RecipientEditTextView;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080073import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080074import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080075import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080076
Mindy Pereira46ce0b12012-01-05 10:32:15 -080077import java.util.ArrayList;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080078import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080079import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080080import java.util.HashSet;
81import java.util.List;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080082import java.util.Set;
Mindy Pereira75f66632012-01-11 11:42:02 -080083import java.util.Map.Entry;
Mindy Pereira82cc5662012-01-09 17:29:30 -080084import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080085
86public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080087 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
88 AttachmentDeletedListener, OnAccountChangedListener {
Mindy Pereira6349a042012-01-04 11:25:01 -080089 // Identifiers for which type of composition this is
90 static final int COMPOSE = -1; // also used for editing a draft
91 static final int REPLY = 0;
92 static final int REPLY_ALL = 1;
93 static final int FORWARD = 2;
94
95 // Integer extra holding one of the above compose action
96 private static final String EXTRA_ACTION = "action";
97
Mindy Pereira82cc5662012-01-09 17:29:30 -080098 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
99 // Map containing information about requests to create new messages, and the id of the
100 // messages that were the result of those requests.
101 //
102 // This map is used when the activity that initiated the save a of a new message, is killed
103 // before the save has completed (and when we know the id of the newly created message). When
104 // a save is completed, the service that is running in the background, will update the map
105 //
106 // When a new ComposeActivity instance is created, it will attempt to use the information in
107 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
108 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
109 // to populate the map with data stored in shared preferences.
110 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
111 // Key used to store the above map
112 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800113 /**
114 * Notifies the {@code Activity} that the caller is an Email
115 * {@code Activity}, so that the back behavior may be modified accordingly.
116 *
117 * @see #onAppUpPressed
118 */
119 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
120
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800121 // If this is a reply/forward then this extra will hold the original message
122 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800123 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800124 private static final String LOG_TAG = new LogUtils().getLogTag();
125 // Request numbers for activities we start
126 private static final int RESULT_PICK_ATTACHMENT = 1;
127 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800128
Mindy Pereira82cc5662012-01-09 17:29:30 -0800129 /**
130 * A single thread for running tasks in the background.
131 */
132 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800133 private RecipientEditTextView mTo;
134 private RecipientEditTextView mCc;
135 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800136 private Button mCcBccButton;
137 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800138 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800139 private Account mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800140 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800141 private TextView mSubject;
142
Mindy Pereira326c6602012-01-04 15:32:42 -0800143 private ComposeModeAdapter mComposeModeAdapter;
144 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800145 private boolean mForward;
146 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800147 private QuotedTextView mQuotedTextView;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800148 private TextView mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800149 private View mFromStatic;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800150 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800151 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800152 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800153 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800154 private boolean mTextChanged;
155 private boolean mReplyFromChanged;
156 private MenuItem mSave;
157 private MenuItem mSend;
158 private Object mDraftIdLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800159 private String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800160 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800161 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800162 private Message mRefMessage;
163
Mindy Pereira326c6602012-01-04 15:32:42 -0800164 /**
165 * Can be called from a non-UI thread.
166 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800167 public static void editDraft(Context launcher, Account account, Message message) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800168 }
169
Mindy Pereira6349a042012-01-04 11:25:01 -0800170 /**
171 * Can be called from a non-UI thread.
172 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800173 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800174 launch(launcher, account, null, COMPOSE);
175 }
176
177 /**
178 * Can be called from a non-UI thread.
179 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800180 public static void reply(Context launcher, Account account, Message message) {
181 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800182 }
183
184 /**
185 * Can be called from a non-UI thread.
186 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800187 public static void replyAll(Context launcher, Account account, Message message) {
188 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800189 }
190
191 /**
192 * Can be called from a non-UI thread.
193 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800194 public static void forward(Context launcher, Account account, Message message) {
195 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800196 }
197
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800198 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800199 Intent intent = new Intent(launcher, ComposeActivity.class);
200 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
201 intent.putExtra(EXTRA_ACTION, action);
202 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800203 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
Mindy Pereira6349a042012-01-04 11:25:01 -0800204 launcher.startActivity(intent);
205 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800206
207 @Override
208 public void onCreate(Bundle savedInstanceState) {
209 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800210 setContentView(R.layout.compose);
211 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800212 Intent intent = getIntent();
213 setAccount((Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
214 if (mAccount == null) {
215 return;
216 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800217 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800218 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereira29ef1b82012-01-13 11:26:21 -0800219 if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800220 initFromRefMessage(action, mAccount.name);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800221 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800222 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800223 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800224 initRecipients();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800225 initActionBar(action);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800226 initFromSpinner();
Mindy Pereira75f66632012-01-11 11:42:02 -0800227 initChangeListeners();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800228 }
229
230 @Override
231 protected void onResume() {
232 super.onResume();
233 // Update the from spinner as other accounts
234 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800235 if (mFromSpinner != null && mAccount != null) {
236 mFromSpinner.asyncInitFromSpinner();
237 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800238 }
239
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800240 @Override
241 protected void onPause() {
242 super.onPause();
243
244 if (mSendConfirmDialog != null) {
245 mSendConfirmDialog.dismiss();
246 }
247 if (mRecipientErrorDialog != null) {
248 mRecipientErrorDialog.dismiss();
249 }
250
251 saveIfNeeded();
252 }
253
254 @Override
255 protected final void onActivityResult(int request, int result, Intent data) {
256 mAddingAttachment = false;
257
258 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
259 addAttachmentAndUpdateView(data);
260 }
261 }
262
263 @Override
264 public final void onSaveInstanceState(Bundle state) {
265 super.onSaveInstanceState(state);
266
267 // onSaveInstanceState is only called if the user might come back to this activity so it is
268 // not an ideal location to save the draft. However, if we have never saved the draft before
269 // we have to save it here in order to have an id to save in the bundle.
270 saveIfNeededOnOrientationChanged();
271 }
272
Mindy Pereira818143e2012-01-11 13:59:49 -0800273 @VisibleForTesting
274 void setAccount(Account account) {
275 mAccount = account;
276 }
277
Mindy Pereira1a95a572012-01-05 12:21:29 -0800278 private void initFromSpinner() {
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800279 mFromSpinner.setCurrentAccount(mAccount);
280 mFromSpinner.asyncInitFromSpinner();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800281 boolean showSpinner = mFromSpinner.getCount() > 1;
282 // If there is only 1 account, just show that account.
283 // Otherwise, give the user the ability to choose which account to send
284 // mail from / save drafts to.
285 mFromStatic.setVisibility(
286 showSpinner ? View.GONE : View.VISIBLE);
287 mFromSpinnerWrapper.setVisibility(
288 showSpinner ? View.VISIBLE : View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800289 }
290
291 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800292 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800293 if (mCcBccButton != null) {
294 mCcBccButton.setOnClickListener(this);
295 }
296 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800297 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira818143e2012-01-11 13:59:49 -0800298 mTo = (RecipientEditTextView) findViewById(R.id.to);
299 mCc = (RecipientEditTextView) findViewById(R.id.cc);
300 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800301 // TODO: add special chips text change watchers before adding
302 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800303 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800304 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
305 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800306 mBodyView = (TextView) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800307 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800308 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800309 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800310 }
311
Mindy Pereira75f66632012-01-11 11:42:02 -0800312 // Now that the message has been initialized from any existing draft or
313 // ref message data, set up listeners for any changes that occur to the
314 // message.
315 private void initChangeListeners() {
316 mSubject.addTextChangedListener(this);
317 mBodyView.addTextChangedListener(this);
318 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
319 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
320 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
321 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800322 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800323 }
324
Mindy Pereira326c6602012-01-04 15:32:42 -0800325 private void initActionBar(int action) {
326 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800327 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800328 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800329 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
330 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800331 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800332 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800333 if (mComposeModeAdapter == null) {
334 mComposeModeAdapter = new ComposeModeAdapter(this);
335 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800336 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
337 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800338 switch (action) {
339 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800340 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800341 break;
342 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800343 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800344 break;
345 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800346 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800347 break;
348 }
349 }
350 }
351
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800352 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800353 if (mRefMessage != null) {
354 mRefMessageId = mRefMessage.refMessageId;
355 setSubject(mRefMessage, action);
356 // Setup recipients
357 if (action == FORWARD) {
358 mForward = true;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800359 }
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800360 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
361 initBodyFromRefMessage(mRefMessage, action);
362 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
363 initAttachments(mRefMessage);
364 }
365 updateHideOrShowCcBcc();
Mindy Pereira6349a042012-01-04 11:25:01 -0800366 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800367 }
368
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800369 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800370 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800371 }
372
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800373 private void initBodyFromRefMessage(Message refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800374 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800375 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
376 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800377 }
378
379 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800380 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800381 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
382 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
383 if (ccVisible || bccVisible) {
384 mCcBccView.show(false, ccVisible, bccVisible);
385 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800386 if (mCcBccButton != null) {
387 if (!mCc.isShown() || !mBcc.isShown()) {
388 mCcBccButton.setVisibility(View.VISIBLE);
389 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
390 : R.string.add_bcc_label));
391 } else {
392 mCcBccButton.setVisibility(View.GONE);
393 }
394 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800395 }
396
Mindy Pereira013194c2012-01-06 15:09:33 -0800397 /**
398 * Add attachment and update the compose area appropriately.
399 * @param data
400 */
401 public void addAttachmentAndUpdateView(Intent data) {
402 Uri uri = data != null ? data.getData() : null;
Mindy Pereira013194c2012-01-06 15:09:33 -0800403 try {
Mindy Pereira7aee8f72012-01-10 16:35:56 -0800404 long size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800405 if (size > 0) {
406 mAttachmentsChanged = true;
407 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800408 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800409 } catch (AttachmentFailureException e) {
410 // A toast has already been shown to the user, no need to do
411 // anything.
412 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800413 }
414 }
415
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800416 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800417 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800418 // Don't populate the address if this is a forward.
419 if (action == ComposeActivity.FORWARD) {
420 return;
421 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800422 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800423 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800424
Mindy Pereira818143e2012-01-11 13:59:49 -0800425 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800426 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800427 // This is the email address of the current user, i.e. the one composing
428 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800429 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800430 String fromAddress = refMessage.from;
431 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
432 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800433 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800434
435 // If this is a reply, the Cc list is empty. If this is a reply-all, the
436 // Cc list is the union of the To and Cc recipients of the original
437 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800438 // already on the To list.
439 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800440 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
441 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800442 addToAddresses(toAddresses);
443 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800444 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800445 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
446 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800447 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800448 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800449 addRecipients(accountEmail, ccAddresses,
450 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800451 addCcAddresses(ccAddresses, toAddresses);
452 }
453 }
454
455 private void addToAddresses(Collection<String> addresses) {
456 addAddressesToList(addresses, mTo);
457 }
458
459 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
460 addCcAddressesToList(tokenizeAddressList(addresses), tokenizeAddressList(toAddresses),
461 mCc);
462 }
463
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800464 @VisibleForTesting
465 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
466 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
467 String address;
468
469 HashSet<String> compareTo = convertToHashSet(compareToList);
470 for (Rfc822Token[] tokens : addresses) {
471 for (int i = 0; i < tokens.length; i++) {
472 address = tokens[i].toString();
473 // Check if this is a duplicate:
474 if (!compareTo.contains(tokens[i].getAddress())) {
475 // Get the address here
476 list.append(address + END_TOKEN);
477 }
478 }
479 }
480 }
481
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800482 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
483 HashSet<String> hash = new HashSet<String>();
484 for (Rfc822Token[] tokens : list) {
485 for (int i = 0; i < tokens.length; i++) {
486 hash.add(tokens[i].getAddress());
487 }
488 }
489 return hash;
490 }
491
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800492 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
493 @VisibleForTesting
494 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
495
496 for (String address: addresses) {
497 tokenized.add(Rfc822Tokenizer.tokenize(address));
498 }
499 return tokenized;
500 }
501
502 @VisibleForTesting
503 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
504 for (String address : addresses) {
505 addAddressToList(address, list);
506 }
507 }
508
509 private void addAddressToList(String address, RecipientEditTextView list) {
510 if (address == null || list == null)
511 return;
512
513 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
514
515 for (int i = 0; i < tokens.length; i++) {
516 list.append(tokens[i] + END_TOKEN);
517 }
518 }
519
520 @VisibleForTesting
521 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800522 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800523 // The To recipient is the reply-to address specified in the original
524 // message, unless it is:
525 // the current user OR a custom from of the current user, in which case
526 // it's the To recipient list of the original message.
527 // OR missing, in which case use the sender of the original message
528 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800529 if (!TextUtils.isEmpty(replyToAddress)) {
530 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800531 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800532 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800533 }
534 return toAddresses;
535 }
536
537 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
538 for (String email : addresses) {
539 // Do not add this account, or any of the custom froms, to the list
540 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800541 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800542 if (!account.equalsIgnoreCase(recipientAddress)) {
543 recipients.add(email.replace("\"\"", ""));
544 }
545 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800546 }
547
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800548 private void setSubject(Message refMessage, int action) {
549 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800550 String prefix;
551 String correctedSubject = null;
552 if (action == ComposeActivity.COMPOSE) {
553 prefix = "";
554 } else if (action == ComposeActivity.FORWARD) {
555 prefix = getString(R.string.forward_subject_label);
556 } else {
557 prefix = getString(R.string.reply_subject_label);
558 }
559
560 // Don't duplicate the prefix
561 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
562 correctedSubject = subject;
563 } else {
564 correctedSubject = String
565 .format(getString(R.string.formatted_subject), prefix, subject);
566 }
567 mSubject.setText(correctedSubject);
568 }
569
Mindy Pereira818143e2012-01-11 13:59:49 -0800570 private void initRecipients() {
571 setupRecipients(mTo);
572 setupRecipients(mCc);
573 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800574 }
575
Mindy Pereira818143e2012-01-11 13:59:49 -0800576 private void setupRecipients(RecipientEditTextView view) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800577 String accountName = mAccount.name;
578 view.setAdapter(new RecipientAdapter(this, accountName));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800579 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800580 if (mValidator == null) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800581 int offset = accountName.indexOf("@") + 1;
582 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800583 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800584 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800585 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800586 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800587 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800588 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800589 }
590
591 @Override
592 public void onClick(View v) {
593 int id = v.getId();
594 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800595 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800596 // Verify that cc/ bcc aren't showing.
597 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800598 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800599 break;
600 }
601 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800602
603 @Override
604 public boolean onCreateOptionsMenu(Menu menu) {
605 super.onCreateOptionsMenu(menu);
606 MenuInflater inflater = getMenuInflater();
607 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800608 mSave = menu.findItem(R.id.save);
609 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800610 return true;
611 }
612
613 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800614 public boolean onPrepareOptionsMenu(Menu menu) {
615 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -0800616 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800617 // Its possible there is a menu item OR a button.
618 boolean ccFieldVisible = mCc.isShown();
619 boolean bccFieldVisible = mBcc.isShown();
620 if (!ccFieldVisible || !bccFieldVisible) {
621 ccBcc.setVisible(true);
622 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
623 : R.string.add_bcc_label));
624 } else {
625 ccBcc.setVisible(false);
626 }
627 }
Mindy Pereira75f66632012-01-11 11:42:02 -0800628 if (mSave != null) {
629 mSave.setEnabled(shouldSave());
630 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800631 return true;
632 }
633
634 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800635 public boolean onOptionsItemSelected(MenuItem item) {
636 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -0800637 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800638 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -0800639 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -0800640 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -0800641 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800642 case R.id.add_cc_bcc:
643 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800644 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800645 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800646 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800647 break;
648 case R.id.send:
649 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -0800650 break;
651 default:
652 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800653 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800654 }
655 return !handled ? super.onOptionsItemSelected(item) : handled;
656 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800657
Mindy Pereira33fe9082012-01-09 16:24:30 -0800658 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800659 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800660 }
661
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800662 private void doSave(boolean showToast, boolean resetIME) {
663 sendOrSaveWithSanityChecks(true, showToast, false);
664 if (resetIME) {
665 // Clear the IME composing suggestions from the body.
666 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
667 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800668 }
669
Mindy Pereira82cc5662012-01-09 17:29:30 -0800670 /*package*/ interface SendOrSaveCallback {
671 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
672 public void notifyMessageIdAllocated(SendOrSaveMessage message, long messageId);
673 public long getMessageId();
674 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
675 }
676
677 /*package*/ static class SendOrSaveTask implements Runnable {
678 private final Context mContext;
679 private final SendOrSaveCallback mSendOrSaveCallback;
680 @VisibleForTesting
681 final SendOrSaveMessage mSendOrSaveMessage;
682
683 public SendOrSaveTask(Context context, SendOrSaveMessage message,
684 SendOrSaveCallback callback) {
685 mContext = context;
686 mSendOrSaveCallback = callback;
687 mSendOrSaveMessage = message;
688 }
689
690 @Override
691 public void run() {
692 final SendOrSaveMessage message = mSendOrSaveMessage;
693
694 final Account selectedAccount = message.mSelectedAccount;
695 long messageId = mSendOrSaveCallback.getMessageId();
696 // If a previous draft has been saved, in an account that is different
697 // than what the user wants to send from, remove the old draft, and treat this
698 // as a new message
699 if (!selectedAccount.equals(message.mAccount)) {
700 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
701 ContentResolver resolver = mContext.getContentResolver();
702 ContentValues values = new ContentValues();
703 values.put(BaseColumns._ID, messageId);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800704 if (!TextUtils.isEmpty(selectedAccount.expungeMessageUri)) {
705 resolver.update(Uri.parse(selectedAccount.expungeMessageUri), values, null,
706 null);
707 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800708 // reset messageId to 0, so a new message will be created
709 messageId = UIProvider.INVALID_MESSAGE_ID;
710 }
711 }
712
713 final long messageIdToSave = messageId;
714 int newDraftId = -1;
715 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
716 mContext.getContentResolver().update(
717 Uri.parse(message.mSave ? selectedAccount.saveDraftUri
718 : selectedAccount.sendMessageUri), message.mValues, null, null);
719 } else {
720 newDraftId = mContext.getContentResolver().update(
721 Uri.parse(message.mSave ? selectedAccount.saveDraftUri
722 : selectedAccount.sendMessageUri), message.mValues, null, null);
723
724 // Broadcast notification that a new message id has been
725 // allocated
726 mSendOrSaveCallback.notifyMessageIdAllocated(message, newDraftId);
727 }
728
729 if (!message.mSave) {
730 UIProvider.incrementRecipientsTimesContacted(mContext,
731 (String) message.mValues.get(UIProvider.MessageColumns.TO));
732 UIProvider.incrementRecipientsTimesContacted(mContext,
733 (String) message.mValues.get(UIProvider.MessageColumns.CC));
734 UIProvider.incrementRecipientsTimesContacted(mContext,
735 (String) message.mValues.get(UIProvider.MessageColumns.BCC));
736 }
737 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
738 }
739 }
740
741 // Array of the outstanding send or save tasks. Access is synchronized
742 // with the object itself
743 /* package for testing */
744 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
745 private int mRequestId;
746 private long mDraftId;
747
748 /*package*/ static class SendOrSaveMessage {
749 final Account mAccount;
750 final Account mSelectedAccount;
751 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800752 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800753 final boolean mSave;
754 final int mRequestId;
755
756 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800757 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800758 mAccount = account;
759 mSelectedAccount = selectedAccount;
760 mValues = values;
761 mRefMessageId = refMessageId;
762 mSave = save;
763 mRequestId = mValues.hashCode() ^ hashCode();
764 }
765
766 int requestId() {
767 return mRequestId;
768 }
769 }
770
771 /**
772 * Get the to recipients.
773 */
774 public String[] getToAddresses() {
775 return getAddressesFromList(mTo);
776 }
777
778 /**
779 * Get the cc recipients.
780 */
781 public String[] getCcAddresses() {
782 return getAddressesFromList(mCc);
783 }
784
785 /**
786 * Get the bcc recipients.
787 */
788 public String[] getBccAddresses() {
789 return getAddressesFromList(mBcc);
790 }
791
792 public String[] getAddressesFromList(RecipientEditTextView list) {
793 if (list == null) {
794 return new String[0];
795 }
796 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
797 int count = tokens.length;
798 String[] result = new String[count];
799 for (int i = 0; i < count; i++) {
800 result[i] = tokens[i].toString();
801 }
802 return result;
803 }
804
805 /**
806 * Check for invalid email addresses.
807 * @param to String array of email addresses to check.
808 * @param wrongEmailsOut Emails addresses that were invalid.
809 */
810 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
811 for (String email : to) {
812 if (!mValidator.isValid(email)) {
813 wrongEmailsOut.add(email);
814 }
815 }
816 }
817
818 /**
819 * Show an error because the user has entered an invalid recipient.
820 * @param message
821 */
822 public void showRecipientErrorDialog(String message) {
823 // Only 1 invalid recipients error dialog should be allowed up at a
824 // time.
825 if (mRecipientErrorDialog != null) {
826 mRecipientErrorDialog.dismiss();
827 }
828 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
829 R.string.recipient_error_dialog_title)
830 .setIconAttribute(android.R.attr.alertDialogIcon)
831 .setCancelable(false)
832 .setPositiveButton(
833 R.string.ok, new Dialog.OnClickListener() {
834 public void onClick(DialogInterface dialog, int which) {
835 // after the user dismisses the recipient error
836 // dialog we want to make sure to refocus the
837 // recipient to field so they can fix the issue
838 // easily
839 if (mTo != null) {
840 mTo.requestFocus();
841 }
842 mRecipientErrorDialog = null;
843 }
844 }).show();
845 }
846
847 /**
848 * Update the state of the UI based on whether or not the current draft
849 * needs to be saved and the message is not empty.
850 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800851 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800852 if (mSave != null) {
853 mSave.setEnabled((shouldSave() && !isBlank()));
854 }
855 }
856
857 /**
858 * Returns true if we need to save the current draft.
859 */
860 private boolean shouldSave() {
861 synchronized (mDraftIdLock) {
862 // The message should only be saved if:
863 // It hasn't been sent AND
864 // Some text has been added to the message OR
865 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800866 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -0800867 (mReplyFromChanged && !isBlank()));
868 }
869 }
870
871 /**
872 * Check if the ComposeArea believes all fields are blank.
873 * @return boolean
874 */
875 public boolean isBlank() {
876 return mSubject.getText().length() == 0
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800877 && mBodyView.getText().length() == 0
Mindy Pereira82cc5662012-01-09 17:29:30 -0800878 && mTo.length() == 0
879 && mCc.length() == 0
880 && mBcc.length() == 0
881 && mAttachmentsView.getAttachments().size() == 0;
882 }
883
884 /**
885 * Allows any changes made by the user to be ignored. Called when the user
886 * decides to discard a draft.
887 */
888 private void discardChanges() {
889 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800890 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800891 mReplyFromChanged = false;
892 }
893
894 /**
895 *
896 * @param body
897 * @param save
898 * @param showToast
899 * @return Whether the send or save succeeded.
900 */
901 protected boolean sendOrSaveWithSanityChecks(final boolean save,
902 final boolean showToast, final boolean orientationChanged) {
903 String[] to, cc, bcc;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800904 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800905
906 if (orientationChanged) {
907 to = cc = bcc = new String[0];
908 } else {
909 to = getToAddresses();
910 cc = getCcAddresses();
911 bcc = getBccAddresses();
912 }
913
914 // Don't let the user send to nobody (but it's okay to save a message with no recipients)
915 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
916 showRecipientErrorDialog(getString(R.string.recipient_needed));
917 return false;
918 }
919
920 List<String> wrongEmails = new ArrayList<String>();
921 if (!save) {
922 checkInvalidEmails(to, wrongEmails);
923 checkInvalidEmails(cc, wrongEmails);
924 checkInvalidEmails(bcc, wrongEmails);
925 }
926
927 // Don't let the user send an email with invalid recipients
928 if (wrongEmails.size() > 0) {
929 String errorText =
930 String.format(getString(R.string.invalid_recipient), wrongEmails.get(0));
931 showRecipientErrorDialog(errorText);
932 return false;
933 }
934
935 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
936 public void onClick(DialogInterface dialog, int which) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800937 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800938 }
939 };
940
941 // Show a warning before sending only if there are no attachments.
942 if (!save) {
943 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
944 boolean warnAboutEmptySubject = isSubjectEmpty();
945 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
946
947 // A warning about an empty body may not be warranted when
948 // forwarding mails, since a common use case is to forward
949 // quoted text and not append any more text.
950 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
951
952 // When we bring up a dialog warning the user about a send,
953 // assume that they accept sending the message. If they do not, the dialog
954 // listener is required to enable sending again.
955 if (warnAboutEmptySubject) {
956 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
957 return true;
958 }
959
960 if (warnAboutEmptyBody) {
961 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
962 return true;
963 }
964 }
965 // Ask for confirmation to send (if always required)
966 if (showSendConfirmation()) {
967 showSendConfirmDialog(R.string.confirm_send_message, listener);
968 return true;
969 }
970 }
971
972 sendOrSave(body, save, showToast, false);
973 return true;
974 }
975
976 /**
977 * Returns a boolean indicating whether warnings should be shown for empty
978 * subject and body fields
979 *
980 * @return True if a warning should be shown for empty text fields
981 */
982 protected boolean showEmptyTextWarnings() {
983 return mAttachmentsView.getAttachments().size() == 0;
984 }
985
986 /**
987 * Returns a boolean indicating whether the user should confirm each send
988 *
989 * @return True if a warning should be on each send
990 */
991 protected boolean showSendConfirmation() {
992 // TODO: read user preference for whether or not to show confirm send dialog.
993 return true;
994 }
995
996 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
997 if (mSendConfirmDialog != null) {
998 mSendConfirmDialog.dismiss();
999 mSendConfirmDialog = null;
1000 }
1001 mSendConfirmDialog = new AlertDialog.Builder(this)
1002 .setMessage(messageId)
1003 .setTitle(R.string.confirm_send_title)
1004 .setIconAttribute(android.R.attr.alertDialogIcon)
1005 .setPositiveButton(R.string.send, listener)
1006 .setNegativeButton(R.string.cancel, this)
1007 .setCancelable(false)
1008 .show();
1009 }
1010
1011 /**
1012 * Returns whether the ComposeArea believes there is any text in the body of
1013 * the composition. TODO: When ComposeArea controls the Body as well, add
1014 * that here.
1015 */
1016 public boolean isBodyEmpty() {
1017 return !mQuotedTextView.isTextIncluded();
1018 }
1019
1020 /**
1021 * Test to see if the subject is empty.
1022 * @return boolean.
1023 */
1024 // TODO: this will likely go away when composeArea.focus() is implemented
1025 // after all the widget control is moved over.
1026 public boolean isSubjectEmpty() {
1027 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1028 }
1029
1030 /* package */
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001031 static int sendOrSaveInternal(Context context, final Account account,
1032 final Account selectedAccount, String fromAddress, final Spanned body,
1033 final String[] to, final String[] cc, final String[] bcc, final String subject,
1034 final CharSequence quotedText, final List<Attachment> attachments,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001035 final String refMessageId, SendOrSaveCallback callback, Handler handler, boolean save,
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001036 boolean forward) {
1037 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001038
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001039 MessageModification.putToAddresses(values, to);
1040 MessageModification.putCcAddresses(values, cc);
1041 MessageModification.putBccAddresses(values, bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001042
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001043 MessageModification.putSubject(values, subject);
1044 String htmlBody = Html.toHtml(body);
1045 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1046 StringBuilder fullBody = new StringBuilder(htmlBody);
1047 if (includeQuotedText) {
1048 if (forward) {
1049 // forwarded messages get full text in HTML from client
1050 fullBody.append(quotedText);
1051 MessageModification.putForward(values, forward);
1052 } else {
1053 // replies get full quoted text from server - HTMl gets
1054 // converted to text for now
1055 final String text = quotedText.toString();
1056 if (QuotedTextView.containsQuotedText(text)) {
1057 int pos = QuotedTextView.getQuotedTextOffset(text);
1058 fullBody.append(text.substring(0, pos));
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001059 MessageModification.putForward(values, forward);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001060 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001061 } else {
1062 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1063 // This shouldn't happen, but just use what we have,
1064 // and don't do server-side expansion
1065 fullBody.append(text);
1066 }
1067 }
1068 }
1069 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1070 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001071
1072 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1073 values, refMessageId, save);
1074 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
1075
1076 callback.initializeSendOrSave(sendOrSaveTask);
1077
1078 // Do the send/save action on the specified handler to avoid possible ANRs
1079 handler.post(sendOrSaveTask);
1080
1081 return sendOrSaveMessage.requestId();
1082 }
1083
1084 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1085 boolean orientationChanged) {
1086 // Check if user is a monkey. Monkeys can compose and hit send
1087 // button but are not allowed to send anything off the device.
1088 if (!save && ActivityManager.isUserAMonkey()) {
1089 return;
1090 }
1091
1092 String[] to, cc, bcc;
1093 if (orientationChanged) {
1094 to = cc = bcc = new String[0];
1095 } else {
1096 to = getToAddresses();
1097 cc = getCcAddresses();
1098 bcc = getBccAddresses();
1099 }
1100
1101
1102 SendOrSaveCallback callback = new SendOrSaveCallback() {
1103 private long mDraftId;
1104 private int mRestoredRequestId;
1105
1106 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
1107 synchronized(mActiveTasks) {
1108 int numTasks = mActiveTasks.size();
1109 if (numTasks == 0) {
1110 // Start service so we won't be killed if this app is put in the
1111 // background.
1112 startService(new Intent(ComposeActivity.this, EmptyService.class));
1113 }
1114
1115 mActiveTasks.add(sendOrSaveTask);
1116 }
1117 if (sTestSendOrSaveCallback != null) {
1118 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1119 }
1120 }
1121
1122 public void notifyMessageIdAllocated(SendOrSaveMessage message, long messageId) {
1123 synchronized(mDraftIdLock) {
1124 mDraftId = messageId;
1125 sRequestMessageIdMap.put(message.requestId(), messageId);
1126
1127 // Cache request message map, in case the process is killed
1128 saveRequestMap();
1129 }
1130 if (sTestSendOrSaveCallback != null) {
1131 sTestSendOrSaveCallback.notifyMessageIdAllocated(message, messageId);
1132 }
1133 }
1134
1135 public long getMessageId() {
1136 synchronized(mDraftIdLock) {
1137 if (mDraftId == UIProvider.INVALID_MESSAGE_ID) {
1138 // We don't have the message Id, check to see if we have a restored
1139 // request id, and see if we have a message for that request.
1140 if (mRestoredRequestId != 0) {
1141 Long retrievedMessageId =
1142 sRequestMessageIdMap.get(mRestoredRequestId);
1143 if (retrievedMessageId != null) {
1144 mDraftId = retrievedMessageId.longValue();
1145 }
1146 }
1147 }
1148 return mDraftId;
1149 }
1150 }
1151
1152 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1153 if (success) {
1154 // Successfully sent or saved so reset change markers
1155 discardChanges();
1156 } else {
1157 // A failure happened with saving/sending the draft
1158 // TODO(pwestbro): add a better string that should be used when failing to
1159 // send or save
1160 Toast.makeText(ComposeActivity.this, R.string.send_failed,
1161 Toast.LENGTH_SHORT).show();
1162 }
1163
1164 int numTasks;
1165 synchronized(mActiveTasks) {
1166 // Remove the task from the list of active tasks
1167 mActiveTasks.remove(task);
1168 numTasks = mActiveTasks.size();
1169 }
1170
1171 if (numTasks == 0) {
1172 // Stop service so we can be killed.
1173 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1174 }
1175 if (sTestSendOrSaveCallback != null) {
1176 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1177 }
1178 }
1179 };
1180
1181 // Get the selected account if the from spinner has been setup.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001182 Account selectedAccount = mAccount;
1183 String fromAddress = selectedAccount.name;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001184 if (selectedAccount == null || fromAddress == null) {
1185 // We don't have either the selected account or from address,
1186 // use mAccount.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001187 selectedAccount = mAccount;
1188 fromAddress = mAccount.name;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001189 }
1190
1191 if (mSendSaveTaskHandler == null) {
1192 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1193 handlerThread.start();
1194
1195 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1196 }
1197
1198 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body,
1199 to, cc, bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedText(),
1200 mAttachmentsView.getAttachments(), mRefMessageId, callback, mSendSaveTaskHandler,
1201 save, mForward);
1202
1203 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1204 mRecipient = selectedAccount.name;
1205 }
1206 mAccount = selectedAccount;
1207
1208 // Don't display the toast if the user is just changing the orientation, but we still
1209 // need to save the draft to the cursor because this is how we restore the attachments
1210 // when the configuration change completes.
1211 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1212 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1213 Toast.LENGTH_LONG).show();
1214 }
1215
1216 // Need to update variables here
1217 // because the send or save completes asynchronously even though the
1218 // toast shows right away.
1219 discardChanges();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001220 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001221
1222 // If we are sending, finish the activity
1223 if (!save) {
1224 finish();
1225 }
1226 }
1227
1228 /**
1229 * Save the state of the request messageid map. This allows for the Gmail process
1230 * to be killed, but and still allow for ComposeActivity instances to be recreated
1231 * correctly.
1232 */
1233 private void saveRequestMap() {
1234 // TODO: store the request map in user preferences.
1235 }
1236
Mindy Pereira013194c2012-01-06 15:09:33 -08001237 public void doAttach() {
1238 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1239 i.addCategory(Intent.CATEGORY_OPENABLE);
1240 if (Settings.System.getInt(
1241 getContentResolver(), UIProvider.getAttachmentTypeSetting(), 0) != 0) {
1242 i.setType("*/*");
1243 } else {
1244 i.setType("image/*");
1245 }
1246 mAddingAttachment = true;
1247 startActivityForResult(Intent.createChooser(i,
1248 getText(R.string.select_attachment_type)), RESULT_PICK_ATTACHMENT);
1249 }
1250
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001251 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001252 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001253 if (mCcBccButton != null) {
1254 mCcBccButton.setVisibility(View.GONE);
1255 }
1256 }
1257
Mindy Pereira326c6602012-01-04 15:32:42 -08001258 @Override
1259 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001260 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001261 if (position == ComposeActivity.REPLY) {
1262 mComposeMode = ComposeActivity.REPLY;
1263 } else if (position == ComposeActivity.REPLY_ALL) {
1264 mComposeMode = ComposeActivity.REPLY_ALL;
1265 } else if (position == ComposeActivity.FORWARD) {
1266 mComposeMode = ComposeActivity.FORWARD;
1267 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001268 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001269 resetMessageForModeChange();
Mindy Pereira33fe9082012-01-09 16:24:30 -08001270 initFromRefMessage(mComposeMode, mAccount.name);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001271 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001272 return true;
1273 }
1274
Mindy Pereira154386a2012-01-11 13:02:33 -08001275 private void resetMessageForModeChange() {
1276 // When switching between reply, reply all, forward,
1277 // follow the behavior of webview.
1278 // The contents of the following fields are cleared
1279 // so that they can be populated directly from the
1280 // ref message:
1281 // 1) Any recipient fields
1282 // 2) The subject
1283 mTo.setText("");
1284 mCc.setText("");
1285 mBcc.setText("");
1286 // Any edits to the subject are replaced with the original subject.
1287 mSubject.setText("");
1288
1289 // Any changes to the contents of the following fields are kept:
1290 // 1) Body
1291 // 2) Attachments
1292 // If the user made changes to attachments, keep their changes.
1293 if (!mAttachmentsChanged) {
1294 mAttachmentsView.deleteAllAttachments();
1295 }
1296 }
1297
Mindy Pereira326c6602012-01-04 15:32:42 -08001298 private class ComposeModeAdapter extends ArrayAdapter<String> {
1299
1300 private LayoutInflater mInflater;
1301
1302 public ComposeModeAdapter(Context context) {
1303 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1304 .getStringArray(R.array.compose_modes));
1305 }
1306
1307 private LayoutInflater getInflater() {
1308 if (mInflater == null) {
1309 mInflater = LayoutInflater.from(getContext());
1310 }
1311 return mInflater;
1312 }
1313
1314 @Override
1315 public View getView(int position, View convertView, ViewGroup parent) {
1316 if (convertView == null) {
1317 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1318 }
1319 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1320 return super.getView(position, convertView, parent);
1321 }
1322 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001323
1324 @Override
1325 public void onRespondInline(String text) {
1326 appendToBody(text, false);
1327 }
1328
1329 /**
1330 * Append text to the body of the message. If there is no existing body
1331 * text, just sets the body to text.
1332 *
1333 * @param text
1334 * @param withSignature True to append a signature.
1335 */
1336 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001337 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001338 if (bodyText != null && bodyText.length() > 0) {
1339 bodyText.append(text);
1340 } else {
1341 setBody(text, withSignature);
1342 }
1343 }
1344
1345 /**
1346 * Set the body of the message.
1347 * @param text
1348 * @param withSignature True to append a signature.
1349 */
1350 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001351 mBodyView.setText(text);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001352 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001353
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001354 @Override
1355 public void onAccountChanged() {
1356 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001357 mAccount = selectedAccountInfo;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001358
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001359 // TODO: handle discarding attachments when switching accounts.
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001360 // Only enable save for this draft if there is any other content
1361 // in the message.
1362 if (!isBlank()) {
1363 enableSave(true);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001364 }
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001365 mReplyFromChanged = true;
Mindy Pereira818143e2012-01-11 13:59:49 -08001366 initRecipients();
Mindy Pereira1a95a572012-01-05 12:21:29 -08001367 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001368
1369 public void enableSave(boolean enabled) {
1370 if (mSave != null) {
1371 mSave.setEnabled(enabled);
1372 }
1373 }
1374
1375 public void enableSend(boolean enabled) {
1376 if (mSend != null) {
1377 mSend.setEnabled(enabled);
1378 }
1379 }
1380
1381 /**
1382 * Handles button clicks from any error dialogs dealing with sending
1383 * a message.
1384 */
1385 @Override
1386 public void onClick(DialogInterface dialog, int which) {
1387 switch (which) {
1388 case DialogInterface.BUTTON_POSITIVE: {
1389 doDiscardWithoutConfirmation(true /* show toast */ );
1390 break;
1391 }
1392 case DialogInterface.BUTTON_NEGATIVE: {
1393 // If the user cancels the send, re-enable the send button.
1394 enableSend(true);
1395 break;
1396 }
1397 }
1398
1399 }
1400
1401 /**
1402 * Effectively discard the current message.
1403 *
1404 * This method is either invoked from the menu or from the dialog
1405 * once the user has confirmed that they want to discard the message.
1406 * @param showToast show "Message discarded" toast if true
1407 */
1408 private void doDiscardWithoutConfirmation(boolean showToast) {
1409 synchronized (mDraftIdLock) {
1410 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1411 ContentValues values = new ContentValues();
1412 values.put(MessageColumns.SERVER_ID, mDraftId);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001413 getContentResolver().update(Uri.parse(mAccount.expungeMessageUri),
Mindy Pereira82cc5662012-01-09 17:29:30 -08001414 values, null, null);
1415 // This is not strictly necessary (since we should not try to
1416 // save the draft after calling this) but it ensures that if we
1417 // do save again for some reason we make a new draft rather than
1418 // trying to resave an expunged draft.
1419 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1420 }
1421 }
1422
1423 if (showToast) {
1424 // Display a toast to let the user know
1425 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1426 }
1427
1428 // This prevents the draft from being saved in onPause().
1429 discardChanges();
1430 finish();
1431 }
1432
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001433 private void saveIfNeeded() {
1434 if (mAccount == null) {
1435 // We have not chosen an account yet so there's no way that we can save. This is ok,
1436 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1437 // user has not interacted with us yet and there is no real state to save.
1438 return;
1439 }
1440
1441 if (shouldSave()) {
1442 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1443 }
1444 }
1445
1446 private void saveIfNeededOnOrientationChanged() {
1447 if (mAccount == null) {
1448 // We have not chosen an account yet so there's no way that we can save. This is ok,
1449 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1450 // user has not interacted with us yet and there is no real state to save.
1451 return;
1452 }
1453
1454 if (shouldSave()) {
1455 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1456 }
1457 }
1458
1459 /**
1460 * Save a draft if a draft already exists or the message is not empty.
1461 */
1462 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1463 saveOnOrientationChanged();
1464 if (resetIME) {
1465 // Clear the IME composing suggestions from the body.
1466 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1467 }
1468 }
1469
1470 protected boolean saveOnOrientationChanged() {
1471 return sendOrSaveWithSanityChecks(true, false, true);
1472 }
1473
1474 @Override
1475 public void onAttachmentDeleted() {
1476 mAttachmentsChanged = true;
1477 updateSaveUi();
1478 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001479
1480
1481 /**
1482 * This is called any time one of our text fields changes.
1483 */
1484 public void afterTextChanged(Editable s) {
1485 mTextChanged = true;
1486 updateSaveUi();
1487 }
1488
1489 @Override
1490 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1491 // Do nothing.
1492 }
1493
1494 public void onTextChanged(CharSequence s, int start, int before, int count) {
1495 // Do nothing.
1496 }
1497
1498
1499 // There is a big difference between the text associated with an address changing
1500 // to add the display name or to format properly and a recipient being added or deleted.
1501 // Make sure we only notify of changes when a recipient has been added or deleted.
1502 private class RecipientTextWatcher implements TextWatcher {
1503 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
1504
1505 private RecipientEditTextView mView;
1506
1507 private TextWatcher mListener;
1508
1509 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
1510 mView = view;
1511 mListener = listener;
1512 }
1513
1514 @Override
1515 public void afterTextChanged(Editable s) {
1516 if (hasChanged()) {
1517 mListener.afterTextChanged(s);
1518 }
1519 }
1520
1521 private boolean hasChanged() {
1522 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
1523 int totalCount = currRecips.length;
1524 int totalPrevCount = 0;
1525 for (Entry<String, Integer> entry : mContent.entrySet()) {
1526 totalPrevCount += entry.getValue();
1527 }
1528 if (totalCount != totalPrevCount) {
1529 return true;
1530 }
1531
1532 for (String recip : currRecips) {
1533 if (!mContent.containsKey(recip)) {
1534 return true;
1535 } else {
1536 int count = mContent.get(recip) - 1;
1537 if (count < 0) {
1538 return true;
1539 } else {
1540 mContent.put(recip, count);
1541 }
1542 }
1543 }
1544 return false;
1545 }
1546
1547 private String[] tokenizeRecips(String[] recips) {
1548 // Tokenize them all and put them in the list.
1549 String[] recipAddresses = new String[recips.length];
1550 for (int i = 0; i < recips.length; i++) {
1551 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
1552 }
1553 return recipAddresses;
1554 }
1555
1556 @Override
1557 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1558 String[] recips = tokenizeRecips(getAddressesFromList(mView));
1559 for (String recip : recips) {
1560 if (!mContent.containsKey(recip)) {
1561 mContent.put(recip, 1);
1562 } else {
1563 mContent.put(recip, (mContent.get(recip)) + 1);
1564 }
1565 }
1566 }
1567
1568 @Override
1569 public void onTextChanged(CharSequence s, int start, int before, int count) {
1570 // Do nothing.
1571 }
1572 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001573}