blob: a329ca697afc465617050a54845af91cee5af4da [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.database.Cursor;
32import 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;
Mindy Pereira82cc5662012-01-09 17:29:30 -080036import android.provider.BaseColumns;
Mindy Pereira013194c2012-01-06 15:09:33 -080037import android.provider.Settings;
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;
Mindy Pereira326c6602012-01-04 15:32:42 -080050import android.view.ViewGroup;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080051import android.view.View.OnClickListener;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080052import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080053import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080054import android.widget.Button;
Mindy Pereira6349a042012-01-04 11:25:01 -080055import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080056import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080057
Mindy Pereirac17d0732011-12-29 10:46:19 -080058import com.android.common.Rfc822Validator;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080059import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080060import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080061import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080062import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080063import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080064import com.android.mail.providers.Address;
65import com.android.mail.providers.Attachment;
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;
78import java.util.Arrays;
79import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080080import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080081import java.util.HashSet;
82import java.util.List;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080083import java.util.Set;
Mindy Pereira75f66632012-01-11 11:42:02 -080084import java.util.Map.Entry;
Mindy Pereira82cc5662012-01-09 17:29:30 -080085import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080086
87public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080088 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
89 AttachmentDeletedListener, OnAccountChangedListener {
Mindy Pereira6349a042012-01-04 11:25:01 -080090 // Identifiers for which type of composition this is
91 static final int COMPOSE = -1; // also used for editing a draft
92 static final int REPLY = 0;
93 static final int REPLY_ALL = 1;
94 static final int FORWARD = 2;
95
96 // Integer extra holding one of the above compose action
97 private static final String EXTRA_ACTION = "action";
98
Mindy Pereira82cc5662012-01-09 17:29:30 -080099 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
100 // Map containing information about requests to create new messages, and the id of the
101 // messages that were the result of those requests.
102 //
103 // This map is used when the activity that initiated the save a of a new message, is killed
104 // before the save has completed (and when we know the id of the newly created message). When
105 // a save is completed, the service that is running in the background, will update the map
106 //
107 // When a new ComposeActivity instance is created, it will attempt to use the information in
108 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
109 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
110 // to populate the map with data stored in shared preferences.
111 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
112 // Key used to store the above map
113 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800114 /**
115 * Notifies the {@code Activity} that the caller is an Email
116 * {@code Activity}, so that the back behavior may be modified accordingly.
117 *
118 * @see #onAppUpPressed
119 */
120 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
121
122 // If this is a reply/forward then this extra will hold the original message uri
123 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-uri";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800124 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800125 private static final String LOG_TAG = new LogUtils().getLogTag();
126 // Request numbers for activities we start
127 private static final int RESULT_PICK_ATTACHMENT = 1;
128 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800129
Mindy Pereira82cc5662012-01-09 17:29:30 -0800130 /**
131 * A single thread for running tasks in the background.
132 */
133 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800134 private RecipientEditTextView mTo;
135 private RecipientEditTextView mCc;
136 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800137 private Button mCcBccButton;
138 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800139 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800140 private Account mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800141 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800142 private Uri mRefMessageUri;
143 private TextView mSubject;
144
Mindy Pereira326c6602012-01-04 15:32:42 -0800145 private ComposeModeAdapter mComposeModeAdapter;
146 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800147 private boolean mForward;
148 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800149 private QuotedTextView mQuotedTextView;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800150 private TextView mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800151 private View mFromStatic;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800152 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800153 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800154 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800155 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800156 private boolean mTextChanged;
157 private boolean mReplyFromChanged;
158 private MenuItem mSave;
159 private MenuItem mSend;
160 private Object mDraftIdLock = new Object();
161 private long mRefMessageId;
162 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800163 private AlertDialog mSendConfirmDialog;
Mindy Pereira326c6602012-01-04 15:32:42 -0800164 /**
165 * Can be called from a non-UI thread.
166 */
Mindy Pereira82cc5662012-01-09 17:29:30 -0800167 public static void editDraft(Context launcher, Account account, long localMessageId) {
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 Pereira33fe9082012-01-09 16:24:30 -0800180 public static void reply(Context launcher, Account account, String uri) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800181 launch(launcher, account, uri, REPLY);
182 }
183
184 /**
185 * Can be called from a non-UI thread.
186 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800187 public static void replyAll(Context launcher, Account account, String uri) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800188 launch(launcher, account, uri, REPLY_ALL);
189 }
190
191 /**
192 * Can be called from a non-UI thread.
193 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800194 public static void forward(Context launcher, Account account, String uri) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800195 launch(launcher, account, uri, FORWARD);
196 }
197
Mindy Pereira33fe9082012-01-09 16:24:30 -0800198 private static void launch(Context launcher, Account account, String uri, 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);
203 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, uri);
204 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 Pereira29ef1b82012-01-13 11:26:21 -0800218 String refUri = intent.getStringExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
219 if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
220 if (TextUtils.isEmpty(refUri)) {
221 LogUtils.w(LOG_TAG,
222 "The message is a reply/forward, but there is not a valid ref message uri");
223 } else {
224 mRefMessageUri = Uri.parse(refUri);
225 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800226 initFromRefMessage(action, mAccount.name);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800227 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800228 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800229 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800230 initRecipients();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800231 initActionBar(action);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800232 initFromSpinner();
Mindy Pereira75f66632012-01-11 11:42:02 -0800233 initChangeListeners();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800234 }
235
236 @Override
237 protected void onResume() {
238 super.onResume();
239 // Update the from spinner as other accounts
240 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800241 if (mFromSpinner != null && mAccount != null) {
242 mFromSpinner.asyncInitFromSpinner();
243 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800244 }
245
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800246 @Override
247 protected void onPause() {
248 super.onPause();
249
250 if (mSendConfirmDialog != null) {
251 mSendConfirmDialog.dismiss();
252 }
253 if (mRecipientErrorDialog != null) {
254 mRecipientErrorDialog.dismiss();
255 }
256
257 saveIfNeeded();
258 }
259
260 @Override
261 protected final void onActivityResult(int request, int result, Intent data) {
262 mAddingAttachment = false;
263
264 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
265 addAttachmentAndUpdateView(data);
266 }
267 }
268
269 @Override
270 public final void onSaveInstanceState(Bundle state) {
271 super.onSaveInstanceState(state);
272
273 // onSaveInstanceState is only called if the user might come back to this activity so it is
274 // not an ideal location to save the draft. However, if we have never saved the draft before
275 // we have to save it here in order to have an id to save in the bundle.
276 saveIfNeededOnOrientationChanged();
277 }
278
Mindy Pereira818143e2012-01-11 13:59:49 -0800279 @VisibleForTesting
280 void setAccount(Account account) {
281 mAccount = account;
282 }
283
Mindy Pereira1a95a572012-01-05 12:21:29 -0800284 private void initFromSpinner() {
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800285 mFromSpinner.setCurrentAccount(mAccount);
286 mFromSpinner.asyncInitFromSpinner();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800287 boolean showSpinner = mFromSpinner.getCount() > 1;
288 // If there is only 1 account, just show that account.
289 // Otherwise, give the user the ability to choose which account to send
290 // mail from / save drafts to.
291 mFromStatic.setVisibility(
292 showSpinner ? View.GONE : View.VISIBLE);
293 mFromSpinnerWrapper.setVisibility(
294 showSpinner ? View.VISIBLE : View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800295 }
296
297 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800298 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800299 if (mCcBccButton != null) {
300 mCcBccButton.setOnClickListener(this);
301 }
302 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800303 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira818143e2012-01-11 13:59:49 -0800304 mTo = (RecipientEditTextView) findViewById(R.id.to);
305 mCc = (RecipientEditTextView) findViewById(R.id.cc);
306 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800307 // TODO: add special chips text change watchers before adding
308 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800309 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800310 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
311 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800312 mBodyView = (TextView) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800313 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800314 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800315 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800316 }
317
Mindy Pereira75f66632012-01-11 11:42:02 -0800318 // Now that the message has been initialized from any existing draft or
319 // ref message data, set up listeners for any changes that occur to the
320 // message.
321 private void initChangeListeners() {
322 mSubject.addTextChangedListener(this);
323 mBodyView.addTextChangedListener(this);
324 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
325 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
326 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
327 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800328 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800329 }
330
Mindy Pereira326c6602012-01-04 15:32:42 -0800331 private void initActionBar(int action) {
332 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800333 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800334 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800335 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
336 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800337 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800338 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800339 if (mComposeModeAdapter == null) {
340 mComposeModeAdapter = new ComposeModeAdapter(this);
341 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800342 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
343 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800344 switch (action) {
345 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800346 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800347 break;
348 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800349 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800350 break;
351 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800352 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800353 break;
354 }
355 }
356 }
357
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800358 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800359 ContentResolver resolver = getContentResolver();
360 Cursor refMessage = resolver.query(mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
361 null, null);
362 if (refMessage != null) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800363 try {
364 refMessage.moveToFirst();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800365 mRefMessageId = refMessage.getLong(UIProvider.MESSAGE_ID_COLUMN);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800366 setSubject(refMessage, action);
367 // Setup recipients
368 if (action == FORWARD) {
369 mForward = true;
370 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800371 initRecipientsFromRefMessageCursor(recipientAddress, refMessage, action);
372 initBodyFromRefMessage(refMessage, action);
373 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800374 initAttachments(refMessage);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800375 }
376 updateHideOrShowCcBcc();
377 } finally {
378 refMessage.close();
379 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800380 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800381 }
382
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800383 private void initAttachments(Cursor refMessage) {
384 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800385 }
386
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800387 private void initBodyFromRefMessage(Cursor refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800388 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800389 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
390 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800391 }
392
393 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800394 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800395 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
396 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
397 if (ccVisible || bccVisible) {
398 mCcBccView.show(false, ccVisible, bccVisible);
399 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800400 if (mCcBccButton != null) {
401 if (!mCc.isShown() || !mBcc.isShown()) {
402 mCcBccButton.setVisibility(View.VISIBLE);
403 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
404 : R.string.add_bcc_label));
405 } else {
406 mCcBccButton.setVisibility(View.GONE);
407 }
408 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800409 }
410
Mindy Pereira013194c2012-01-06 15:09:33 -0800411 /**
412 * Add attachment and update the compose area appropriately.
413 * @param data
414 */
415 public void addAttachmentAndUpdateView(Intent data) {
416 Uri uri = data != null ? data.getData() : null;
Mindy Pereira013194c2012-01-06 15:09:33 -0800417 try {
Mindy Pereira7aee8f72012-01-10 16:35:56 -0800418 long size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800419 if (size > 0) {
420 mAttachmentsChanged = true;
421 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800422 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800423 } catch (AttachmentFailureException e) {
424 // A toast has already been shown to the user, no need to do
425 // anything.
426 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800427 }
428 }
429
Mindy Pereira818143e2012-01-11 13:59:49 -0800430 void initRecipientsFromRefMessageCursor(String recipientAddress, Cursor refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800431 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800432 // Don't populate the address if this is a forward.
433 if (action == ComposeActivity.FORWARD) {
434 return;
435 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800436 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800437 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800438
Mindy Pereira818143e2012-01-11 13:59:49 -0800439 @VisibleForTesting
440 void initReplyRecipients(String account, Cursor refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800441 // This is the email address of the current user, i.e. the one composing
442 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800443 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800444 String fromAddress = refMessage.getString(UIProvider.MESSAGE_FROM_COLUMN);
445 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage
446 .getString(UIProvider.MESSAGE_TO_COLUMN));
447 String[] replytoAddresses = Utils.splitCommaSeparatedString(refMessage
448 .getString(UIProvider.MESSAGE_REPLY_TO_COLUMN));
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800449 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800450
451 // If this is a reply, the Cc list is empty. If this is a reply-all, the
452 // Cc list is the union of the To and Cc recipients of the original
453 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800454 // already on the To list.
455 if (action == ComposeActivity.REPLY) {
456 toAddresses = initToRecipients(account, accountEmail, fromAddress,
457 replytoAddresses, new String[0]);
458 addToAddresses(toAddresses);
459 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800460 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800461 toAddresses = initToRecipients(account, accountEmail, fromAddress,
462 replytoAddresses, new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800463 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800464 addRecipients(accountEmail, ccAddresses, sentToAddresses);
465 addRecipients(accountEmail, ccAddresses, Utils.splitCommaSeparatedString(refMessage
466 .getString(UIProvider.MESSAGE_CC_COLUMN)));
467 addCcAddresses(ccAddresses, toAddresses);
468 }
469 }
470
471 private void addToAddresses(Collection<String> addresses) {
472 addAddressesToList(addresses, mTo);
473 }
474
475 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
476 addCcAddressesToList(tokenizeAddressList(addresses), tokenizeAddressList(toAddresses),
477 mCc);
478 }
479
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800480 @VisibleForTesting
481 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
482 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
483 String address;
484
485 HashSet<String> compareTo = convertToHashSet(compareToList);
486 for (Rfc822Token[] tokens : addresses) {
487 for (int i = 0; i < tokens.length; i++) {
488 address = tokens[i].toString();
489 // Check if this is a duplicate:
490 if (!compareTo.contains(tokens[i].getAddress())) {
491 // Get the address here
492 list.append(address + END_TOKEN);
493 }
494 }
495 }
496 }
497
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800498 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
499 HashSet<String> hash = new HashSet<String>();
500 for (Rfc822Token[] tokens : list) {
501 for (int i = 0; i < tokens.length; i++) {
502 hash.add(tokens[i].getAddress());
503 }
504 }
505 return hash;
506 }
507
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800508 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
509 @VisibleForTesting
510 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
511
512 for (String address: addresses) {
513 tokenized.add(Rfc822Tokenizer.tokenize(address));
514 }
515 return tokenized;
516 }
517
518 @VisibleForTesting
519 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
520 for (String address : addresses) {
521 addAddressToList(address, list);
522 }
523 }
524
525 private void addAddressToList(String address, RecipientEditTextView list) {
526 if (address == null || list == null)
527 return;
528
529 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
530
531 for (int i = 0; i < tokens.length; i++) {
532 list.append(tokens[i] + END_TOKEN);
533 }
534 }
535
536 @VisibleForTesting
537 protected Collection<String> initToRecipients(String account, String accountEmail,
538 String senderAddress, String[] replyToAddresses, String[] inToAddresses) {
539 // The To recipient is the reply-to address specified in the original
540 // message, unless it is:
541 // the current user OR a custom from of the current user, in which case
542 // it's the To recipient list of the original message.
543 // OR missing, in which case use the sender of the original message
544 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira4a20b702012-01-05 16:24:24 -0800545 Address sender = Address.getEmailAddress(senderAddress);
546 if (sender != null && sender.getAddress().equalsIgnoreCase(account)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800547 // The sender address is this account, so reply acts like reply all.
548 toAddresses.addAll(Arrays.asList(inToAddresses));
549 } else if (replyToAddresses != null && replyToAddresses.length != 0) {
550 toAddresses.addAll(Arrays.asList(replyToAddresses));
551 } else {
552 // Check to see if the sender address is one of the user's custom
553 // from addresses.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800554 if (senderAddress != null && sender != null
555 && !accountEmail.equalsIgnoreCase(sender.getAddress())) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800556 // Replying to the sender of the original message is the most
557 // common case.
558 toAddresses.add(senderAddress);
559 } else {
560 // This happens if the user replies to a message they originally
561 // wrote. In this case, "reply" really means "re-send," so we
562 // target the original recipients. This works as expected even
563 // if the user sent the original message to themselves.
564 toAddresses.addAll(Arrays.asList(inToAddresses));
565 }
566 }
567 return toAddresses;
568 }
569
570 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
571 for (String email : addresses) {
572 // Do not add this account, or any of the custom froms, to the list
573 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800574 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800575 if (!account.equalsIgnoreCase(recipientAddress)) {
576 recipients.add(email.replace("\"\"", ""));
577 }
578 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800579 }
580
581 private void setSubject(Cursor refMessage, int action) {
582 String subject = refMessage.getString(UIProvider.MESSAGE_SUBJECT_COLUMN);
583 String prefix;
584 String correctedSubject = null;
585 if (action == ComposeActivity.COMPOSE) {
586 prefix = "";
587 } else if (action == ComposeActivity.FORWARD) {
588 prefix = getString(R.string.forward_subject_label);
589 } else {
590 prefix = getString(R.string.reply_subject_label);
591 }
592
593 // Don't duplicate the prefix
594 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
595 correctedSubject = subject;
596 } else {
597 correctedSubject = String
598 .format(getString(R.string.formatted_subject), prefix, subject);
599 }
600 mSubject.setText(correctedSubject);
601 }
602
Mindy Pereira818143e2012-01-11 13:59:49 -0800603 private void initRecipients() {
604 setupRecipients(mTo);
605 setupRecipients(mCc);
606 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800607 }
608
Mindy Pereira818143e2012-01-11 13:59:49 -0800609 private void setupRecipients(RecipientEditTextView view) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800610 String accountName = mAccount.name;
611 view.setAdapter(new RecipientAdapter(this, accountName));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800612 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800613 if (mValidator == null) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800614 int offset = accountName.indexOf("@") + 1;
615 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800616 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800617 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800618 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800619 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800620 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800621 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800622 }
623
624 @Override
625 public void onClick(View v) {
626 int id = v.getId();
627 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800628 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800629 // Verify that cc/ bcc aren't showing.
630 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800631 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800632 break;
633 }
634 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800635
636 @Override
637 public boolean onCreateOptionsMenu(Menu menu) {
638 super.onCreateOptionsMenu(menu);
639 MenuInflater inflater = getMenuInflater();
640 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800641 mSave = menu.findItem(R.id.save);
642 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800643 return true;
644 }
645
646 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800647 public boolean onPrepareOptionsMenu(Menu menu) {
648 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -0800649 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800650 // Its possible there is a menu item OR a button.
651 boolean ccFieldVisible = mCc.isShown();
652 boolean bccFieldVisible = mBcc.isShown();
653 if (!ccFieldVisible || !bccFieldVisible) {
654 ccBcc.setVisible(true);
655 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
656 : R.string.add_bcc_label));
657 } else {
658 ccBcc.setVisible(false);
659 }
660 }
Mindy Pereira75f66632012-01-11 11:42:02 -0800661 if (mSave != null) {
662 mSave.setEnabled(shouldSave());
663 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800664 return true;
665 }
666
667 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800668 public boolean onOptionsItemSelected(MenuItem item) {
669 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -0800670 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800671 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -0800672 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -0800673 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -0800674 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800675 case R.id.add_cc_bcc:
676 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800677 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800678 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800679 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800680 break;
681 case R.id.send:
682 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -0800683 break;
684 default:
685 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800686 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800687 }
688 return !handled ? super.onOptionsItemSelected(item) : handled;
689 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800690
Mindy Pereira33fe9082012-01-09 16:24:30 -0800691 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800692 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800693 }
694
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800695 private void doSave(boolean showToast, boolean resetIME) {
696 sendOrSaveWithSanityChecks(true, showToast, false);
697 if (resetIME) {
698 // Clear the IME composing suggestions from the body.
699 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
700 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800701 }
702
Mindy Pereira82cc5662012-01-09 17:29:30 -0800703 /*package*/ interface SendOrSaveCallback {
704 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
705 public void notifyMessageIdAllocated(SendOrSaveMessage message, long messageId);
706 public long getMessageId();
707 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
708 }
709
710 /*package*/ static class SendOrSaveTask implements Runnable {
711 private final Context mContext;
712 private final SendOrSaveCallback mSendOrSaveCallback;
713 @VisibleForTesting
714 final SendOrSaveMessage mSendOrSaveMessage;
715
716 public SendOrSaveTask(Context context, SendOrSaveMessage message,
717 SendOrSaveCallback callback) {
718 mContext = context;
719 mSendOrSaveCallback = callback;
720 mSendOrSaveMessage = message;
721 }
722
723 @Override
724 public void run() {
725 final SendOrSaveMessage message = mSendOrSaveMessage;
726
727 final Account selectedAccount = message.mSelectedAccount;
728 long messageId = mSendOrSaveCallback.getMessageId();
729 // If a previous draft has been saved, in an account that is different
730 // than what the user wants to send from, remove the old draft, and treat this
731 // as a new message
732 if (!selectedAccount.equals(message.mAccount)) {
733 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
734 ContentResolver resolver = mContext.getContentResolver();
735 ContentValues values = new ContentValues();
736 values.put(BaseColumns._ID, messageId);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800737 if (!TextUtils.isEmpty(selectedAccount.expungeMessageUri)) {
738 resolver.update(Uri.parse(selectedAccount.expungeMessageUri), values, null,
739 null);
740 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800741 // reset messageId to 0, so a new message will be created
742 messageId = UIProvider.INVALID_MESSAGE_ID;
743 }
744 }
745
746 final long messageIdToSave = messageId;
747 int newDraftId = -1;
748 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
749 mContext.getContentResolver().update(
750 Uri.parse(message.mSave ? selectedAccount.saveDraftUri
751 : selectedAccount.sendMessageUri), message.mValues, null, null);
752 } else {
753 newDraftId = mContext.getContentResolver().update(
754 Uri.parse(message.mSave ? selectedAccount.saveDraftUri
755 : selectedAccount.sendMessageUri), message.mValues, null, null);
756
757 // Broadcast notification that a new message id has been
758 // allocated
759 mSendOrSaveCallback.notifyMessageIdAllocated(message, newDraftId);
760 }
761
762 if (!message.mSave) {
763 UIProvider.incrementRecipientsTimesContacted(mContext,
764 (String) message.mValues.get(UIProvider.MessageColumns.TO));
765 UIProvider.incrementRecipientsTimesContacted(mContext,
766 (String) message.mValues.get(UIProvider.MessageColumns.CC));
767 UIProvider.incrementRecipientsTimesContacted(mContext,
768 (String) message.mValues.get(UIProvider.MessageColumns.BCC));
769 }
770 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
771 }
772 }
773
774 // Array of the outstanding send or save tasks. Access is synchronized
775 // with the object itself
776 /* package for testing */
777 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
778 private int mRequestId;
779 private long mDraftId;
780
781 /*package*/ static class SendOrSaveMessage {
782 final Account mAccount;
783 final Account mSelectedAccount;
784 final ContentValues mValues;
785 final long mRefMessageId;
786 final boolean mSave;
787 final int mRequestId;
788
789 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
790 long refMessageId, boolean save) {
791 mAccount = account;
792 mSelectedAccount = selectedAccount;
793 mValues = values;
794 mRefMessageId = refMessageId;
795 mSave = save;
796 mRequestId = mValues.hashCode() ^ hashCode();
797 }
798
799 int requestId() {
800 return mRequestId;
801 }
802 }
803
804 /**
805 * Get the to recipients.
806 */
807 public String[] getToAddresses() {
808 return getAddressesFromList(mTo);
809 }
810
811 /**
812 * Get the cc recipients.
813 */
814 public String[] getCcAddresses() {
815 return getAddressesFromList(mCc);
816 }
817
818 /**
819 * Get the bcc recipients.
820 */
821 public String[] getBccAddresses() {
822 return getAddressesFromList(mBcc);
823 }
824
825 public String[] getAddressesFromList(RecipientEditTextView list) {
826 if (list == null) {
827 return new String[0];
828 }
829 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
830 int count = tokens.length;
831 String[] result = new String[count];
832 for (int i = 0; i < count; i++) {
833 result[i] = tokens[i].toString();
834 }
835 return result;
836 }
837
838 /**
839 * Check for invalid email addresses.
840 * @param to String array of email addresses to check.
841 * @param wrongEmailsOut Emails addresses that were invalid.
842 */
843 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
844 for (String email : to) {
845 if (!mValidator.isValid(email)) {
846 wrongEmailsOut.add(email);
847 }
848 }
849 }
850
851 /**
852 * Show an error because the user has entered an invalid recipient.
853 * @param message
854 */
855 public void showRecipientErrorDialog(String message) {
856 // Only 1 invalid recipients error dialog should be allowed up at a
857 // time.
858 if (mRecipientErrorDialog != null) {
859 mRecipientErrorDialog.dismiss();
860 }
861 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
862 R.string.recipient_error_dialog_title)
863 .setIconAttribute(android.R.attr.alertDialogIcon)
864 .setCancelable(false)
865 .setPositiveButton(
866 R.string.ok, new Dialog.OnClickListener() {
867 public void onClick(DialogInterface dialog, int which) {
868 // after the user dismisses the recipient error
869 // dialog we want to make sure to refocus the
870 // recipient to field so they can fix the issue
871 // easily
872 if (mTo != null) {
873 mTo.requestFocus();
874 }
875 mRecipientErrorDialog = null;
876 }
877 }).show();
878 }
879
880 /**
881 * Update the state of the UI based on whether or not the current draft
882 * needs to be saved and the message is not empty.
883 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800884 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800885 if (mSave != null) {
886 mSave.setEnabled((shouldSave() && !isBlank()));
887 }
888 }
889
890 /**
891 * Returns true if we need to save the current draft.
892 */
893 private boolean shouldSave() {
894 synchronized (mDraftIdLock) {
895 // The message should only be saved if:
896 // It hasn't been sent AND
897 // Some text has been added to the message OR
898 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800899 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -0800900 (mReplyFromChanged && !isBlank()));
901 }
902 }
903
904 /**
905 * Check if the ComposeArea believes all fields are blank.
906 * @return boolean
907 */
908 public boolean isBlank() {
909 return mSubject.getText().length() == 0
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800910 && mBodyView.getText().length() == 0
Mindy Pereira82cc5662012-01-09 17:29:30 -0800911 && mTo.length() == 0
912 && mCc.length() == 0
913 && mBcc.length() == 0
914 && mAttachmentsView.getAttachments().size() == 0;
915 }
916
917 /**
918 * Allows any changes made by the user to be ignored. Called when the user
919 * decides to discard a draft.
920 */
921 private void discardChanges() {
922 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800923 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800924 mReplyFromChanged = false;
925 }
926
927 /**
928 *
929 * @param body
930 * @param save
931 * @param showToast
932 * @return Whether the send or save succeeded.
933 */
934 protected boolean sendOrSaveWithSanityChecks(final boolean save,
935 final boolean showToast, final boolean orientationChanged) {
936 String[] to, cc, bcc;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800937 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800938
939 if (orientationChanged) {
940 to = cc = bcc = new String[0];
941 } else {
942 to = getToAddresses();
943 cc = getCcAddresses();
944 bcc = getBccAddresses();
945 }
946
947 // Don't let the user send to nobody (but it's okay to save a message with no recipients)
948 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
949 showRecipientErrorDialog(getString(R.string.recipient_needed));
950 return false;
951 }
952
953 List<String> wrongEmails = new ArrayList<String>();
954 if (!save) {
955 checkInvalidEmails(to, wrongEmails);
956 checkInvalidEmails(cc, wrongEmails);
957 checkInvalidEmails(bcc, wrongEmails);
958 }
959
960 // Don't let the user send an email with invalid recipients
961 if (wrongEmails.size() > 0) {
962 String errorText =
963 String.format(getString(R.string.invalid_recipient), wrongEmails.get(0));
964 showRecipientErrorDialog(errorText);
965 return false;
966 }
967
968 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
969 public void onClick(DialogInterface dialog, int which) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800970 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800971 }
972 };
973
974 // Show a warning before sending only if there are no attachments.
975 if (!save) {
976 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
977 boolean warnAboutEmptySubject = isSubjectEmpty();
978 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
979
980 // A warning about an empty body may not be warranted when
981 // forwarding mails, since a common use case is to forward
982 // quoted text and not append any more text.
983 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
984
985 // When we bring up a dialog warning the user about a send,
986 // assume that they accept sending the message. If they do not, the dialog
987 // listener is required to enable sending again.
988 if (warnAboutEmptySubject) {
989 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
990 return true;
991 }
992
993 if (warnAboutEmptyBody) {
994 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
995 return true;
996 }
997 }
998 // Ask for confirmation to send (if always required)
999 if (showSendConfirmation()) {
1000 showSendConfirmDialog(R.string.confirm_send_message, listener);
1001 return true;
1002 }
1003 }
1004
1005 sendOrSave(body, save, showToast, false);
1006 return true;
1007 }
1008
1009 /**
1010 * Returns a boolean indicating whether warnings should be shown for empty
1011 * subject and body fields
1012 *
1013 * @return True if a warning should be shown for empty text fields
1014 */
1015 protected boolean showEmptyTextWarnings() {
1016 return mAttachmentsView.getAttachments().size() == 0;
1017 }
1018
1019 /**
1020 * Returns a boolean indicating whether the user should confirm each send
1021 *
1022 * @return True if a warning should be on each send
1023 */
1024 protected boolean showSendConfirmation() {
1025 // TODO: read user preference for whether or not to show confirm send dialog.
1026 return true;
1027 }
1028
1029 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1030 if (mSendConfirmDialog != null) {
1031 mSendConfirmDialog.dismiss();
1032 mSendConfirmDialog = null;
1033 }
1034 mSendConfirmDialog = new AlertDialog.Builder(this)
1035 .setMessage(messageId)
1036 .setTitle(R.string.confirm_send_title)
1037 .setIconAttribute(android.R.attr.alertDialogIcon)
1038 .setPositiveButton(R.string.send, listener)
1039 .setNegativeButton(R.string.cancel, this)
1040 .setCancelable(false)
1041 .show();
1042 }
1043
1044 /**
1045 * Returns whether the ComposeArea believes there is any text in the body of
1046 * the composition. TODO: When ComposeArea controls the Body as well, add
1047 * that here.
1048 */
1049 public boolean isBodyEmpty() {
1050 return !mQuotedTextView.isTextIncluded();
1051 }
1052
1053 /**
1054 * Test to see if the subject is empty.
1055 * @return boolean.
1056 */
1057 // TODO: this will likely go away when composeArea.focus() is implemented
1058 // after all the widget control is moved over.
1059 public boolean isSubjectEmpty() {
1060 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1061 }
1062
1063 /* package */
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001064 static int sendOrSaveInternal(Context context, final Account account,
1065 final Account selectedAccount, String fromAddress, final Spanned body,
1066 final String[] to, final String[] cc, final String[] bcc, final String subject,
1067 final CharSequence quotedText, final List<Attachment> attachments,
1068 final long refMessageId, SendOrSaveCallback callback, Handler handler, boolean save,
1069 boolean forward) {
1070 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001071
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001072 MessageModification.putToAddresses(values, to);
1073 MessageModification.putCcAddresses(values, cc);
1074 MessageModification.putBccAddresses(values, bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001075
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001076 MessageModification.putSubject(values, subject);
1077 String htmlBody = Html.toHtml(body);
1078 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1079 StringBuilder fullBody = new StringBuilder(htmlBody);
1080 if (includeQuotedText) {
1081 if (forward) {
1082 // forwarded messages get full text in HTML from client
1083 fullBody.append(quotedText);
1084 MessageModification.putForward(values, forward);
1085 } else {
1086 // replies get full quoted text from server - HTMl gets
1087 // converted to text for now
1088 final String text = quotedText.toString();
1089 if (QuotedTextView.containsQuotedText(text)) {
1090 int pos = QuotedTextView.getQuotedTextOffset(text);
1091 fullBody.append(text.substring(0, pos));
1092 int quoteStartPos = fullBody.length();
1093 MessageModification.putForward(values, forward);
1094 MessageModification.putIncludeQuotedText(values, includeQuotedText);
1095 MessageModification.putQuoteStartPos(values, quoteStartPos);
1096 } else {
1097 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1098 // This shouldn't happen, but just use what we have,
1099 // and don't do server-side expansion
1100 fullBody.append(text);
1101 }
1102 }
1103 }
1104 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1105 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001106
1107 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1108 values, refMessageId, save);
1109 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
1110
1111 callback.initializeSendOrSave(sendOrSaveTask);
1112
1113 // Do the send/save action on the specified handler to avoid possible ANRs
1114 handler.post(sendOrSaveTask);
1115
1116 return sendOrSaveMessage.requestId();
1117 }
1118
1119 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1120 boolean orientationChanged) {
1121 // Check if user is a monkey. Monkeys can compose and hit send
1122 // button but are not allowed to send anything off the device.
1123 if (!save && ActivityManager.isUserAMonkey()) {
1124 return;
1125 }
1126
1127 String[] to, cc, bcc;
1128 if (orientationChanged) {
1129 to = cc = bcc = new String[0];
1130 } else {
1131 to = getToAddresses();
1132 cc = getCcAddresses();
1133 bcc = getBccAddresses();
1134 }
1135
1136
1137 SendOrSaveCallback callback = new SendOrSaveCallback() {
1138 private long mDraftId;
1139 private int mRestoredRequestId;
1140
1141 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
1142 synchronized(mActiveTasks) {
1143 int numTasks = mActiveTasks.size();
1144 if (numTasks == 0) {
1145 // Start service so we won't be killed if this app is put in the
1146 // background.
1147 startService(new Intent(ComposeActivity.this, EmptyService.class));
1148 }
1149
1150 mActiveTasks.add(sendOrSaveTask);
1151 }
1152 if (sTestSendOrSaveCallback != null) {
1153 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1154 }
1155 }
1156
1157 public void notifyMessageIdAllocated(SendOrSaveMessage message, long messageId) {
1158 synchronized(mDraftIdLock) {
1159 mDraftId = messageId;
1160 sRequestMessageIdMap.put(message.requestId(), messageId);
1161
1162 // Cache request message map, in case the process is killed
1163 saveRequestMap();
1164 }
1165 if (sTestSendOrSaveCallback != null) {
1166 sTestSendOrSaveCallback.notifyMessageIdAllocated(message, messageId);
1167 }
1168 }
1169
1170 public long getMessageId() {
1171 synchronized(mDraftIdLock) {
1172 if (mDraftId == UIProvider.INVALID_MESSAGE_ID) {
1173 // We don't have the message Id, check to see if we have a restored
1174 // request id, and see if we have a message for that request.
1175 if (mRestoredRequestId != 0) {
1176 Long retrievedMessageId =
1177 sRequestMessageIdMap.get(mRestoredRequestId);
1178 if (retrievedMessageId != null) {
1179 mDraftId = retrievedMessageId.longValue();
1180 }
1181 }
1182 }
1183 return mDraftId;
1184 }
1185 }
1186
1187 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1188 if (success) {
1189 // Successfully sent or saved so reset change markers
1190 discardChanges();
1191 } else {
1192 // A failure happened with saving/sending the draft
1193 // TODO(pwestbro): add a better string that should be used when failing to
1194 // send or save
1195 Toast.makeText(ComposeActivity.this, R.string.send_failed,
1196 Toast.LENGTH_SHORT).show();
1197 }
1198
1199 int numTasks;
1200 synchronized(mActiveTasks) {
1201 // Remove the task from the list of active tasks
1202 mActiveTasks.remove(task);
1203 numTasks = mActiveTasks.size();
1204 }
1205
1206 if (numTasks == 0) {
1207 // Stop service so we can be killed.
1208 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1209 }
1210 if (sTestSendOrSaveCallback != null) {
1211 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1212 }
1213 }
1214 };
1215
1216 // Get the selected account if the from spinner has been setup.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001217 Account selectedAccount = mAccount;
1218 String fromAddress = selectedAccount.name;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001219 if (selectedAccount == null || fromAddress == null) {
1220 // We don't have either the selected account or from address,
1221 // use mAccount.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001222 selectedAccount = mAccount;
1223 fromAddress = mAccount.name;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001224 }
1225
1226 if (mSendSaveTaskHandler == null) {
1227 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1228 handlerThread.start();
1229
1230 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1231 }
1232
1233 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body,
1234 to, cc, bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedText(),
1235 mAttachmentsView.getAttachments(), mRefMessageId, callback, mSendSaveTaskHandler,
1236 save, mForward);
1237
1238 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1239 mRecipient = selectedAccount.name;
1240 }
1241 mAccount = selectedAccount;
1242
1243 // Don't display the toast if the user is just changing the orientation, but we still
1244 // need to save the draft to the cursor because this is how we restore the attachments
1245 // when the configuration change completes.
1246 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1247 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1248 Toast.LENGTH_LONG).show();
1249 }
1250
1251 // Need to update variables here
1252 // because the send or save completes asynchronously even though the
1253 // toast shows right away.
1254 discardChanges();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001255 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001256
1257 // If we are sending, finish the activity
1258 if (!save) {
1259 finish();
1260 }
1261 }
1262
1263 /**
1264 * Save the state of the request messageid map. This allows for the Gmail process
1265 * to be killed, but and still allow for ComposeActivity instances to be recreated
1266 * correctly.
1267 */
1268 private void saveRequestMap() {
1269 // TODO: store the request map in user preferences.
1270 }
1271
Mindy Pereira013194c2012-01-06 15:09:33 -08001272 public void doAttach() {
1273 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1274 i.addCategory(Intent.CATEGORY_OPENABLE);
1275 if (Settings.System.getInt(
1276 getContentResolver(), UIProvider.getAttachmentTypeSetting(), 0) != 0) {
1277 i.setType("*/*");
1278 } else {
1279 i.setType("image/*");
1280 }
1281 mAddingAttachment = true;
1282 startActivityForResult(Intent.createChooser(i,
1283 getText(R.string.select_attachment_type)), RESULT_PICK_ATTACHMENT);
1284 }
1285
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001286 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001287 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001288 if (mCcBccButton != null) {
1289 mCcBccButton.setVisibility(View.GONE);
1290 }
1291 }
1292
Mindy Pereira326c6602012-01-04 15:32:42 -08001293 @Override
1294 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001295 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001296 if (position == ComposeActivity.REPLY) {
1297 mComposeMode = ComposeActivity.REPLY;
1298 } else if (position == ComposeActivity.REPLY_ALL) {
1299 mComposeMode = ComposeActivity.REPLY_ALL;
1300 } else if (position == ComposeActivity.FORWARD) {
1301 mComposeMode = ComposeActivity.FORWARD;
1302 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001303 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001304 resetMessageForModeChange();
Mindy Pereira33fe9082012-01-09 16:24:30 -08001305 initFromRefMessage(mComposeMode, mAccount.name);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001306 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001307 return true;
1308 }
1309
Mindy Pereira154386a2012-01-11 13:02:33 -08001310 private void resetMessageForModeChange() {
1311 // When switching between reply, reply all, forward,
1312 // follow the behavior of webview.
1313 // The contents of the following fields are cleared
1314 // so that they can be populated directly from the
1315 // ref message:
1316 // 1) Any recipient fields
1317 // 2) The subject
1318 mTo.setText("");
1319 mCc.setText("");
1320 mBcc.setText("");
1321 // Any edits to the subject are replaced with the original subject.
1322 mSubject.setText("");
1323
1324 // Any changes to the contents of the following fields are kept:
1325 // 1) Body
1326 // 2) Attachments
1327 // If the user made changes to attachments, keep their changes.
1328 if (!mAttachmentsChanged) {
1329 mAttachmentsView.deleteAllAttachments();
1330 }
1331 }
1332
Mindy Pereira326c6602012-01-04 15:32:42 -08001333 private class ComposeModeAdapter extends ArrayAdapter<String> {
1334
1335 private LayoutInflater mInflater;
1336
1337 public ComposeModeAdapter(Context context) {
1338 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1339 .getStringArray(R.array.compose_modes));
1340 }
1341
1342 private LayoutInflater getInflater() {
1343 if (mInflater == null) {
1344 mInflater = LayoutInflater.from(getContext());
1345 }
1346 return mInflater;
1347 }
1348
1349 @Override
1350 public View getView(int position, View convertView, ViewGroup parent) {
1351 if (convertView == null) {
1352 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1353 }
1354 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1355 return super.getView(position, convertView, parent);
1356 }
1357 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001358
1359 @Override
1360 public void onRespondInline(String text) {
1361 appendToBody(text, false);
1362 }
1363
1364 /**
1365 * Append text to the body of the message. If there is no existing body
1366 * text, just sets the body to text.
1367 *
1368 * @param text
1369 * @param withSignature True to append a signature.
1370 */
1371 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001372 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001373 if (bodyText != null && bodyText.length() > 0) {
1374 bodyText.append(text);
1375 } else {
1376 setBody(text, withSignature);
1377 }
1378 }
1379
1380 /**
1381 * Set the body of the message.
1382 * @param text
1383 * @param withSignature True to append a signature.
1384 */
1385 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001386 mBodyView.setText(text);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001387 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001388
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001389 @Override
1390 public void onAccountChanged() {
1391 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001392 mAccount = selectedAccountInfo;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001393
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001394 // TODO: handle discarding attachments when switching accounts.
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001395 // Only enable save for this draft if there is any other content
1396 // in the message.
1397 if (!isBlank()) {
1398 enableSave(true);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001399 }
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001400 mReplyFromChanged = true;
Mindy Pereira818143e2012-01-11 13:59:49 -08001401 initRecipients();
Mindy Pereira1a95a572012-01-05 12:21:29 -08001402 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001403
1404 public void enableSave(boolean enabled) {
1405 if (mSave != null) {
1406 mSave.setEnabled(enabled);
1407 }
1408 }
1409
1410 public void enableSend(boolean enabled) {
1411 if (mSend != null) {
1412 mSend.setEnabled(enabled);
1413 }
1414 }
1415
1416 /**
1417 * Handles button clicks from any error dialogs dealing with sending
1418 * a message.
1419 */
1420 @Override
1421 public void onClick(DialogInterface dialog, int which) {
1422 switch (which) {
1423 case DialogInterface.BUTTON_POSITIVE: {
1424 doDiscardWithoutConfirmation(true /* show toast */ );
1425 break;
1426 }
1427 case DialogInterface.BUTTON_NEGATIVE: {
1428 // If the user cancels the send, re-enable the send button.
1429 enableSend(true);
1430 break;
1431 }
1432 }
1433
1434 }
1435
1436 /**
1437 * Effectively discard the current message.
1438 *
1439 * This method is either invoked from the menu or from the dialog
1440 * once the user has confirmed that they want to discard the message.
1441 * @param showToast show "Message discarded" toast if true
1442 */
1443 private void doDiscardWithoutConfirmation(boolean showToast) {
1444 synchronized (mDraftIdLock) {
1445 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1446 ContentValues values = new ContentValues();
1447 values.put(MessageColumns.SERVER_ID, mDraftId);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001448 getContentResolver().update(Uri.parse(mAccount.expungeMessageUri),
Mindy Pereira82cc5662012-01-09 17:29:30 -08001449 values, null, null);
1450 // This is not strictly necessary (since we should not try to
1451 // save the draft after calling this) but it ensures that if we
1452 // do save again for some reason we make a new draft rather than
1453 // trying to resave an expunged draft.
1454 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1455 }
1456 }
1457
1458 if (showToast) {
1459 // Display a toast to let the user know
1460 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1461 }
1462
1463 // This prevents the draft from being saved in onPause().
1464 discardChanges();
1465 finish();
1466 }
1467
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001468 private void saveIfNeeded() {
1469 if (mAccount == null) {
1470 // We have not chosen an account yet so there's no way that we can save. This is ok,
1471 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1472 // user has not interacted with us yet and there is no real state to save.
1473 return;
1474 }
1475
1476 if (shouldSave()) {
1477 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1478 }
1479 }
1480
1481 private void saveIfNeededOnOrientationChanged() {
1482 if (mAccount == null) {
1483 // We have not chosen an account yet so there's no way that we can save. This is ok,
1484 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1485 // user has not interacted with us yet and there is no real state to save.
1486 return;
1487 }
1488
1489 if (shouldSave()) {
1490 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1491 }
1492 }
1493
1494 /**
1495 * Save a draft if a draft already exists or the message is not empty.
1496 */
1497 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1498 saveOnOrientationChanged();
1499 if (resetIME) {
1500 // Clear the IME composing suggestions from the body.
1501 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1502 }
1503 }
1504
1505 protected boolean saveOnOrientationChanged() {
1506 return sendOrSaveWithSanityChecks(true, false, true);
1507 }
1508
1509 @Override
1510 public void onAttachmentDeleted() {
1511 mAttachmentsChanged = true;
1512 updateSaveUi();
1513 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001514
1515
1516 /**
1517 * This is called any time one of our text fields changes.
1518 */
1519 public void afterTextChanged(Editable s) {
1520 mTextChanged = true;
1521 updateSaveUi();
1522 }
1523
1524 @Override
1525 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1526 // Do nothing.
1527 }
1528
1529 public void onTextChanged(CharSequence s, int start, int before, int count) {
1530 // Do nothing.
1531 }
1532
1533
1534 // There is a big difference between the text associated with an address changing
1535 // to add the display name or to format properly and a recipient being added or deleted.
1536 // Make sure we only notify of changes when a recipient has been added or deleted.
1537 private class RecipientTextWatcher implements TextWatcher {
1538 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
1539
1540 private RecipientEditTextView mView;
1541
1542 private TextWatcher mListener;
1543
1544 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
1545 mView = view;
1546 mListener = listener;
1547 }
1548
1549 @Override
1550 public void afterTextChanged(Editable s) {
1551 if (hasChanged()) {
1552 mListener.afterTextChanged(s);
1553 }
1554 }
1555
1556 private boolean hasChanged() {
1557 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
1558 int totalCount = currRecips.length;
1559 int totalPrevCount = 0;
1560 for (Entry<String, Integer> entry : mContent.entrySet()) {
1561 totalPrevCount += entry.getValue();
1562 }
1563 if (totalCount != totalPrevCount) {
1564 return true;
1565 }
1566
1567 for (String recip : currRecips) {
1568 if (!mContent.containsKey(recip)) {
1569 return true;
1570 } else {
1571 int count = mContent.get(recip) - 1;
1572 if (count < 0) {
1573 return true;
1574 } else {
1575 mContent.put(recip, count);
1576 }
1577 }
1578 }
1579 return false;
1580 }
1581
1582 private String[] tokenizeRecips(String[] recips) {
1583 // Tokenize them all and put them in the list.
1584 String[] recipAddresses = new String[recips.length];
1585 for (int i = 0; i < recips.length; i++) {
1586 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
1587 }
1588 return recipAddresses;
1589 }
1590
1591 @Override
1592 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1593 String[] recips = tokenizeRecips(getAddressesFromList(mView));
1594 for (String recip : recips) {
1595 if (!mContent.containsKey(recip)) {
1596 mContent.put(recip, 1);
1597 } else {
1598 mContent.put(recip, (mContent.get(recip)) + 1);
1599 }
1600 }
1601 }
1602
1603 @Override
1604 public void onTextChanged(CharSequence s, int start, int before, int count) {
1605 // Do nothing.
1606 }
1607 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001608}