blob: 24fab042a77ba871c5f53b128d89adb868830d4d [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 Pereira46ce0b12012-01-05 10:32:15 -0800218 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
219 mRefMessageUri = Uri.parse(intent.getStringExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI));
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 Pereira6349a042012-01-04 11:25:01 -0800353 ContentResolver resolver = getContentResolver();
354 Cursor refMessage = resolver.query(mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
355 null, null);
356 if (refMessage != null) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800357 try {
358 refMessage.moveToFirst();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800359 mRefMessageId = refMessage.getLong(UIProvider.MESSAGE_ID_COLUMN);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800360 setSubject(refMessage, action);
361 // Setup recipients
362 if (action == FORWARD) {
363 mForward = true;
364 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800365 initRecipientsFromRefMessageCursor(recipientAddress, refMessage, action);
366 initBodyFromRefMessage(refMessage, action);
367 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800368 initAttachments(refMessage);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800369 }
370 updateHideOrShowCcBcc();
371 } finally {
372 refMessage.close();
373 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800374 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800375 }
376
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800377 private void initAttachments(Cursor refMessage) {
378 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800379 }
380
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800381 private void initBodyFromRefMessage(Cursor refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800382 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800383 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
384 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800385 }
386
387 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800388 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800389 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
390 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
391 if (ccVisible || bccVisible) {
392 mCcBccView.show(false, ccVisible, bccVisible);
393 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800394 if (mCcBccButton != null) {
395 if (!mCc.isShown() || !mBcc.isShown()) {
396 mCcBccButton.setVisibility(View.VISIBLE);
397 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
398 : R.string.add_bcc_label));
399 } else {
400 mCcBccButton.setVisibility(View.GONE);
401 }
402 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800403 }
404
Mindy Pereira013194c2012-01-06 15:09:33 -0800405 /**
406 * Add attachment and update the compose area appropriately.
407 * @param data
408 */
409 public void addAttachmentAndUpdateView(Intent data) {
410 Uri uri = data != null ? data.getData() : null;
Mindy Pereira013194c2012-01-06 15:09:33 -0800411 try {
Mindy Pereira7aee8f72012-01-10 16:35:56 -0800412 long size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800413 if (size > 0) {
414 mAttachmentsChanged = true;
415 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800416 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800417 } catch (AttachmentFailureException e) {
418 // A toast has already been shown to the user, no need to do
419 // anything.
420 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800421 }
422 }
423
Mindy Pereira818143e2012-01-11 13:59:49 -0800424 void initRecipientsFromRefMessageCursor(String recipientAddress, Cursor refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800425 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800426 // Don't populate the address if this is a forward.
427 if (action == ComposeActivity.FORWARD) {
428 return;
429 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800430 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800431 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800432
Mindy Pereira818143e2012-01-11 13:59:49 -0800433 @VisibleForTesting
434 void initReplyRecipients(String account, Cursor refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800435 // This is the email address of the current user, i.e. the one composing
436 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800437 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800438 String fromAddress = refMessage.getString(UIProvider.MESSAGE_FROM_COLUMN);
439 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage
440 .getString(UIProvider.MESSAGE_TO_COLUMN));
441 String[] replytoAddresses = Utils.splitCommaSeparatedString(refMessage
442 .getString(UIProvider.MESSAGE_REPLY_TO_COLUMN));
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800443 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800444
445 // If this is a reply, the Cc list is empty. If this is a reply-all, the
446 // Cc list is the union of the To and Cc recipients of the original
447 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800448 // already on the To list.
449 if (action == ComposeActivity.REPLY) {
450 toAddresses = initToRecipients(account, accountEmail, fromAddress,
451 replytoAddresses, new String[0]);
452 addToAddresses(toAddresses);
453 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800454 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800455 toAddresses = initToRecipients(account, accountEmail, fromAddress,
456 replytoAddresses, new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800457 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800458 addRecipients(accountEmail, ccAddresses, sentToAddresses);
459 addRecipients(accountEmail, ccAddresses, Utils.splitCommaSeparatedString(refMessage
460 .getString(UIProvider.MESSAGE_CC_COLUMN)));
461 addCcAddresses(ccAddresses, toAddresses);
462 }
463 }
464
465 private void addToAddresses(Collection<String> addresses) {
466 addAddressesToList(addresses, mTo);
467 }
468
469 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
470 addCcAddressesToList(tokenizeAddressList(addresses), tokenizeAddressList(toAddresses),
471 mCc);
472 }
473
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800474 @VisibleForTesting
475 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
476 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
477 String address;
478
479 HashSet<String> compareTo = convertToHashSet(compareToList);
480 for (Rfc822Token[] tokens : addresses) {
481 for (int i = 0; i < tokens.length; i++) {
482 address = tokens[i].toString();
483 // Check if this is a duplicate:
484 if (!compareTo.contains(tokens[i].getAddress())) {
485 // Get the address here
486 list.append(address + END_TOKEN);
487 }
488 }
489 }
490 }
491
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800492 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
493 HashSet<String> hash = new HashSet<String>();
494 for (Rfc822Token[] tokens : list) {
495 for (int i = 0; i < tokens.length; i++) {
496 hash.add(tokens[i].getAddress());
497 }
498 }
499 return hash;
500 }
501
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800502 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
503 @VisibleForTesting
504 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
505
506 for (String address: addresses) {
507 tokenized.add(Rfc822Tokenizer.tokenize(address));
508 }
509 return tokenized;
510 }
511
512 @VisibleForTesting
513 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
514 for (String address : addresses) {
515 addAddressToList(address, list);
516 }
517 }
518
519 private void addAddressToList(String address, RecipientEditTextView list) {
520 if (address == null || list == null)
521 return;
522
523 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
524
525 for (int i = 0; i < tokens.length; i++) {
526 list.append(tokens[i] + END_TOKEN);
527 }
528 }
529
530 @VisibleForTesting
531 protected Collection<String> initToRecipients(String account, String accountEmail,
532 String senderAddress, String[] replyToAddresses, String[] inToAddresses) {
533 // The To recipient is the reply-to address specified in the original
534 // message, unless it is:
535 // the current user OR a custom from of the current user, in which case
536 // it's the To recipient list of the original message.
537 // OR missing, in which case use the sender of the original message
538 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira4a20b702012-01-05 16:24:24 -0800539 Address sender = Address.getEmailAddress(senderAddress);
540 if (sender != null && sender.getAddress().equalsIgnoreCase(account)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800541 // The sender address is this account, so reply acts like reply all.
542 toAddresses.addAll(Arrays.asList(inToAddresses));
543 } else if (replyToAddresses != null && replyToAddresses.length != 0) {
544 toAddresses.addAll(Arrays.asList(replyToAddresses));
545 } else {
546 // Check to see if the sender address is one of the user's custom
547 // from addresses.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800548 if (senderAddress != null && sender != null
549 && !accountEmail.equalsIgnoreCase(sender.getAddress())) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800550 // Replying to the sender of the original message is the most
551 // common case.
552 toAddresses.add(senderAddress);
553 } else {
554 // This happens if the user replies to a message they originally
555 // wrote. In this case, "reply" really means "re-send," so we
556 // target the original recipients. This works as expected even
557 // if the user sent the original message to themselves.
558 toAddresses.addAll(Arrays.asList(inToAddresses));
559 }
560 }
561 return toAddresses;
562 }
563
564 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
565 for (String email : addresses) {
566 // Do not add this account, or any of the custom froms, to the list
567 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800568 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800569 if (!account.equalsIgnoreCase(recipientAddress)) {
570 recipients.add(email.replace("\"\"", ""));
571 }
572 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800573 }
574
575 private void setSubject(Cursor refMessage, int action) {
576 String subject = refMessage.getString(UIProvider.MESSAGE_SUBJECT_COLUMN);
577 String prefix;
578 String correctedSubject = null;
579 if (action == ComposeActivity.COMPOSE) {
580 prefix = "";
581 } else if (action == ComposeActivity.FORWARD) {
582 prefix = getString(R.string.forward_subject_label);
583 } else {
584 prefix = getString(R.string.reply_subject_label);
585 }
586
587 // Don't duplicate the prefix
588 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
589 correctedSubject = subject;
590 } else {
591 correctedSubject = String
592 .format(getString(R.string.formatted_subject), prefix, subject);
593 }
594 mSubject.setText(correctedSubject);
595 }
596
Mindy Pereira818143e2012-01-11 13:59:49 -0800597 private void initRecipients() {
598 setupRecipients(mTo);
599 setupRecipients(mCc);
600 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800601 }
602
Mindy Pereira818143e2012-01-11 13:59:49 -0800603 private void setupRecipients(RecipientEditTextView view) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800604 String accountName = mAccount.name;
605 view.setAdapter(new RecipientAdapter(this, accountName));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800606 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800607 if (mValidator == null) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800608 int offset = accountName.indexOf("@") + 1;
609 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800610 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800611 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800612 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800613 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800614 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800615 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800616 }
617
618 @Override
619 public void onClick(View v) {
620 int id = v.getId();
621 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800622 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800623 // Verify that cc/ bcc aren't showing.
624 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800625 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800626 break;
627 }
628 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800629
630 @Override
631 public boolean onCreateOptionsMenu(Menu menu) {
632 super.onCreateOptionsMenu(menu);
633 MenuInflater inflater = getMenuInflater();
634 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800635 mSave = menu.findItem(R.id.save);
636 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800637 return true;
638 }
639
640 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800641 public boolean onPrepareOptionsMenu(Menu menu) {
642 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -0800643 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800644 // Its possible there is a menu item OR a button.
645 boolean ccFieldVisible = mCc.isShown();
646 boolean bccFieldVisible = mBcc.isShown();
647 if (!ccFieldVisible || !bccFieldVisible) {
648 ccBcc.setVisible(true);
649 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
650 : R.string.add_bcc_label));
651 } else {
652 ccBcc.setVisible(false);
653 }
654 }
Mindy Pereira75f66632012-01-11 11:42:02 -0800655 if (mSave != null) {
656 mSave.setEnabled(shouldSave());
657 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800658 return true;
659 }
660
661 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800662 public boolean onOptionsItemSelected(MenuItem item) {
663 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -0800664 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800665 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -0800666 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -0800667 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -0800668 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800669 case R.id.add_cc_bcc:
670 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800671 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800672 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800673 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800674 break;
675 case R.id.send:
676 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -0800677 break;
678 default:
679 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800680 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800681 }
682 return !handled ? super.onOptionsItemSelected(item) : handled;
683 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800684
Mindy Pereira33fe9082012-01-09 16:24:30 -0800685 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800686 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800687 }
688
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800689 private void doSave(boolean showToast, boolean resetIME) {
690 sendOrSaveWithSanityChecks(true, showToast, false);
691 if (resetIME) {
692 // Clear the IME composing suggestions from the body.
693 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
694 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800695 }
696
Mindy Pereira82cc5662012-01-09 17:29:30 -0800697 /*package*/ interface SendOrSaveCallback {
698 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
699 public void notifyMessageIdAllocated(SendOrSaveMessage message, long messageId);
700 public long getMessageId();
701 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
702 }
703
704 /*package*/ static class SendOrSaveTask implements Runnable {
705 private final Context mContext;
706 private final SendOrSaveCallback mSendOrSaveCallback;
707 @VisibleForTesting
708 final SendOrSaveMessage mSendOrSaveMessage;
709
710 public SendOrSaveTask(Context context, SendOrSaveMessage message,
711 SendOrSaveCallback callback) {
712 mContext = context;
713 mSendOrSaveCallback = callback;
714 mSendOrSaveMessage = message;
715 }
716
717 @Override
718 public void run() {
719 final SendOrSaveMessage message = mSendOrSaveMessage;
720
721 final Account selectedAccount = message.mSelectedAccount;
722 long messageId = mSendOrSaveCallback.getMessageId();
723 // If a previous draft has been saved, in an account that is different
724 // than what the user wants to send from, remove the old draft, and treat this
725 // as a new message
726 if (!selectedAccount.equals(message.mAccount)) {
727 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
728 ContentResolver resolver = mContext.getContentResolver();
729 ContentValues values = new ContentValues();
730 values.put(BaseColumns._ID, messageId);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800731 if (!TextUtils.isEmpty(selectedAccount.expungeMessageUri)) {
732 resolver.update(Uri.parse(selectedAccount.expungeMessageUri), values, null,
733 null);
734 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800735 // reset messageId to 0, so a new message will be created
736 messageId = UIProvider.INVALID_MESSAGE_ID;
737 }
738 }
739
740 final long messageIdToSave = messageId;
741 int newDraftId = -1;
742 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
743 mContext.getContentResolver().update(
744 Uri.parse(message.mSave ? selectedAccount.saveDraftUri
745 : selectedAccount.sendMessageUri), message.mValues, null, null);
746 } else {
747 newDraftId = mContext.getContentResolver().update(
748 Uri.parse(message.mSave ? selectedAccount.saveDraftUri
749 : selectedAccount.sendMessageUri), message.mValues, null, null);
750
751 // Broadcast notification that a new message id has been
752 // allocated
753 mSendOrSaveCallback.notifyMessageIdAllocated(message, newDraftId);
754 }
755
756 if (!message.mSave) {
757 UIProvider.incrementRecipientsTimesContacted(mContext,
758 (String) message.mValues.get(UIProvider.MessageColumns.TO));
759 UIProvider.incrementRecipientsTimesContacted(mContext,
760 (String) message.mValues.get(UIProvider.MessageColumns.CC));
761 UIProvider.incrementRecipientsTimesContacted(mContext,
762 (String) message.mValues.get(UIProvider.MessageColumns.BCC));
763 }
764 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
765 }
766 }
767
768 // Array of the outstanding send or save tasks. Access is synchronized
769 // with the object itself
770 /* package for testing */
771 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
772 private int mRequestId;
773 private long mDraftId;
774
775 /*package*/ static class SendOrSaveMessage {
776 final Account mAccount;
777 final Account mSelectedAccount;
778 final ContentValues mValues;
779 final long mRefMessageId;
780 final boolean mSave;
781 final int mRequestId;
782
783 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
784 long refMessageId, boolean save) {
785 mAccount = account;
786 mSelectedAccount = selectedAccount;
787 mValues = values;
788 mRefMessageId = refMessageId;
789 mSave = save;
790 mRequestId = mValues.hashCode() ^ hashCode();
791 }
792
793 int requestId() {
794 return mRequestId;
795 }
796 }
797
798 /**
799 * Get the to recipients.
800 */
801 public String[] getToAddresses() {
802 return getAddressesFromList(mTo);
803 }
804
805 /**
806 * Get the cc recipients.
807 */
808 public String[] getCcAddresses() {
809 return getAddressesFromList(mCc);
810 }
811
812 /**
813 * Get the bcc recipients.
814 */
815 public String[] getBccAddresses() {
816 return getAddressesFromList(mBcc);
817 }
818
819 public String[] getAddressesFromList(RecipientEditTextView list) {
820 if (list == null) {
821 return new String[0];
822 }
823 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
824 int count = tokens.length;
825 String[] result = new String[count];
826 for (int i = 0; i < count; i++) {
827 result[i] = tokens[i].toString();
828 }
829 return result;
830 }
831
832 /**
833 * Check for invalid email addresses.
834 * @param to String array of email addresses to check.
835 * @param wrongEmailsOut Emails addresses that were invalid.
836 */
837 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
838 for (String email : to) {
839 if (!mValidator.isValid(email)) {
840 wrongEmailsOut.add(email);
841 }
842 }
843 }
844
845 /**
846 * Show an error because the user has entered an invalid recipient.
847 * @param message
848 */
849 public void showRecipientErrorDialog(String message) {
850 // Only 1 invalid recipients error dialog should be allowed up at a
851 // time.
852 if (mRecipientErrorDialog != null) {
853 mRecipientErrorDialog.dismiss();
854 }
855 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
856 R.string.recipient_error_dialog_title)
857 .setIconAttribute(android.R.attr.alertDialogIcon)
858 .setCancelable(false)
859 .setPositiveButton(
860 R.string.ok, new Dialog.OnClickListener() {
861 public void onClick(DialogInterface dialog, int which) {
862 // after the user dismisses the recipient error
863 // dialog we want to make sure to refocus the
864 // recipient to field so they can fix the issue
865 // easily
866 if (mTo != null) {
867 mTo.requestFocus();
868 }
869 mRecipientErrorDialog = null;
870 }
871 }).show();
872 }
873
874 /**
875 * Update the state of the UI based on whether or not the current draft
876 * needs to be saved and the message is not empty.
877 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800878 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800879 if (mSave != null) {
880 mSave.setEnabled((shouldSave() && !isBlank()));
881 }
882 }
883
884 /**
885 * Returns true if we need to save the current draft.
886 */
887 private boolean shouldSave() {
888 synchronized (mDraftIdLock) {
889 // The message should only be saved if:
890 // It hasn't been sent AND
891 // Some text has been added to the message OR
892 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800893 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -0800894 (mReplyFromChanged && !isBlank()));
895 }
896 }
897
898 /**
899 * Check if the ComposeArea believes all fields are blank.
900 * @return boolean
901 */
902 public boolean isBlank() {
903 return mSubject.getText().length() == 0
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800904 && mBodyView.getText().length() == 0
Mindy Pereira82cc5662012-01-09 17:29:30 -0800905 && mTo.length() == 0
906 && mCc.length() == 0
907 && mBcc.length() == 0
908 && mAttachmentsView.getAttachments().size() == 0;
909 }
910
911 /**
912 * Allows any changes made by the user to be ignored. Called when the user
913 * decides to discard a draft.
914 */
915 private void discardChanges() {
916 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800917 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800918 mReplyFromChanged = false;
919 }
920
921 /**
922 *
923 * @param body
924 * @param save
925 * @param showToast
926 * @return Whether the send or save succeeded.
927 */
928 protected boolean sendOrSaveWithSanityChecks(final boolean save,
929 final boolean showToast, final boolean orientationChanged) {
930 String[] to, cc, bcc;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800931 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800932
933 if (orientationChanged) {
934 to = cc = bcc = new String[0];
935 } else {
936 to = getToAddresses();
937 cc = getCcAddresses();
938 bcc = getBccAddresses();
939 }
940
941 // Don't let the user send to nobody (but it's okay to save a message with no recipients)
942 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
943 showRecipientErrorDialog(getString(R.string.recipient_needed));
944 return false;
945 }
946
947 List<String> wrongEmails = new ArrayList<String>();
948 if (!save) {
949 checkInvalidEmails(to, wrongEmails);
950 checkInvalidEmails(cc, wrongEmails);
951 checkInvalidEmails(bcc, wrongEmails);
952 }
953
954 // Don't let the user send an email with invalid recipients
955 if (wrongEmails.size() > 0) {
956 String errorText =
957 String.format(getString(R.string.invalid_recipient), wrongEmails.get(0));
958 showRecipientErrorDialog(errorText);
959 return false;
960 }
961
962 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
963 public void onClick(DialogInterface dialog, int which) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800964 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800965 }
966 };
967
968 // Show a warning before sending only if there are no attachments.
969 if (!save) {
970 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
971 boolean warnAboutEmptySubject = isSubjectEmpty();
972 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
973
974 // A warning about an empty body may not be warranted when
975 // forwarding mails, since a common use case is to forward
976 // quoted text and not append any more text.
977 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
978
979 // When we bring up a dialog warning the user about a send,
980 // assume that they accept sending the message. If they do not, the dialog
981 // listener is required to enable sending again.
982 if (warnAboutEmptySubject) {
983 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
984 return true;
985 }
986
987 if (warnAboutEmptyBody) {
988 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
989 return true;
990 }
991 }
992 // Ask for confirmation to send (if always required)
993 if (showSendConfirmation()) {
994 showSendConfirmDialog(R.string.confirm_send_message, listener);
995 return true;
996 }
997 }
998
999 sendOrSave(body, save, showToast, false);
1000 return true;
1001 }
1002
1003 /**
1004 * Returns a boolean indicating whether warnings should be shown for empty
1005 * subject and body fields
1006 *
1007 * @return True if a warning should be shown for empty text fields
1008 */
1009 protected boolean showEmptyTextWarnings() {
1010 return mAttachmentsView.getAttachments().size() == 0;
1011 }
1012
1013 /**
1014 * Returns a boolean indicating whether the user should confirm each send
1015 *
1016 * @return True if a warning should be on each send
1017 */
1018 protected boolean showSendConfirmation() {
1019 // TODO: read user preference for whether or not to show confirm send dialog.
1020 return true;
1021 }
1022
1023 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1024 if (mSendConfirmDialog != null) {
1025 mSendConfirmDialog.dismiss();
1026 mSendConfirmDialog = null;
1027 }
1028 mSendConfirmDialog = new AlertDialog.Builder(this)
1029 .setMessage(messageId)
1030 .setTitle(R.string.confirm_send_title)
1031 .setIconAttribute(android.R.attr.alertDialogIcon)
1032 .setPositiveButton(R.string.send, listener)
1033 .setNegativeButton(R.string.cancel, this)
1034 .setCancelable(false)
1035 .show();
1036 }
1037
1038 /**
1039 * Returns whether the ComposeArea believes there is any text in the body of
1040 * the composition. TODO: When ComposeArea controls the Body as well, add
1041 * that here.
1042 */
1043 public boolean isBodyEmpty() {
1044 return !mQuotedTextView.isTextIncluded();
1045 }
1046
1047 /**
1048 * Test to see if the subject is empty.
1049 * @return boolean.
1050 */
1051 // TODO: this will likely go away when composeArea.focus() is implemented
1052 // after all the widget control is moved over.
1053 public boolean isSubjectEmpty() {
1054 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1055 }
1056
1057 /* package */
1058 static int sendOrSaveInternal(Context context, final Account account,
1059 final Account selectedAccount, String fromAddress, final Spanned body, final String[] to,
1060 final String[] cc, final String[] bcc, final String subject,
1061 final CharSequence quotedText, final List<Attachment> attachments,
1062 final long refMessageId, SendOrSaveCallback callback, Handler handler, boolean save,
1063 boolean forward) {
1064 ContentValues values = new ContentValues();
1065
1066 MessageModification.putToAddresses(values, to);
1067 MessageModification.putCcAddresses(values, cc);
1068 MessageModification.putBccAddresses(values, bcc);
1069
1070 MessageModification.putSubject(values, subject);
1071 String htmlBody = Html.toHtml(body);
1072 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1073 StringBuilder fullBody = new StringBuilder(htmlBody);
1074 if (includeQuotedText) {
1075 if (forward) {
1076 // forwarded messages get full text in HTML from client
1077 fullBody.append(quotedText);
1078 MessageModification.putForward(values, forward);
1079 } else {
1080 // replies get full quoted text from server - HTMl gets converted to text for now
1081 final String text = quotedText.toString();
Mindy Pereira913897e2012-01-10 11:27:55 -08001082 if (QuotedTextView.containsQuotedText(text)) {
1083 int pos = QuotedTextView.getQuotedTextOffset(text);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001084 fullBody.append(text.substring(0, pos));
1085 int quoteStartPos = fullBody.length();
1086 MessageModification.putForward(values, forward);
1087 MessageModification.putIncludeQuotedText(values, includeQuotedText);
1088 MessageModification.putQuoteStartPos(values, quoteStartPos);
1089 } else {
1090 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1091 // This shouldn't happen, but just use what we have,
1092 // and don't do server-side expansion
1093 fullBody.append(text);
1094 }
1095 }
1096 }
1097 MessageModification.putBody(values, fullBody.toString());
1098
1099 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1100 values, refMessageId, save);
1101 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
1102
1103 callback.initializeSendOrSave(sendOrSaveTask);
1104
1105 // Do the send/save action on the specified handler to avoid possible ANRs
1106 handler.post(sendOrSaveTask);
1107
1108 return sendOrSaveMessage.requestId();
1109 }
1110
1111 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1112 boolean orientationChanged) {
1113 // Check if user is a monkey. Monkeys can compose and hit send
1114 // button but are not allowed to send anything off the device.
1115 if (!save && ActivityManager.isUserAMonkey()) {
1116 return;
1117 }
1118
1119 String[] to, cc, bcc;
1120 if (orientationChanged) {
1121 to = cc = bcc = new String[0];
1122 } else {
1123 to = getToAddresses();
1124 cc = getCcAddresses();
1125 bcc = getBccAddresses();
1126 }
1127
1128
1129 SendOrSaveCallback callback = new SendOrSaveCallback() {
1130 private long mDraftId;
1131 private int mRestoredRequestId;
1132
1133 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
1134 synchronized(mActiveTasks) {
1135 int numTasks = mActiveTasks.size();
1136 if (numTasks == 0) {
1137 // Start service so we won't be killed if this app is put in the
1138 // background.
1139 startService(new Intent(ComposeActivity.this, EmptyService.class));
1140 }
1141
1142 mActiveTasks.add(sendOrSaveTask);
1143 }
1144 if (sTestSendOrSaveCallback != null) {
1145 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1146 }
1147 }
1148
1149 public void notifyMessageIdAllocated(SendOrSaveMessage message, long messageId) {
1150 synchronized(mDraftIdLock) {
1151 mDraftId = messageId;
1152 sRequestMessageIdMap.put(message.requestId(), messageId);
1153
1154 // Cache request message map, in case the process is killed
1155 saveRequestMap();
1156 }
1157 if (sTestSendOrSaveCallback != null) {
1158 sTestSendOrSaveCallback.notifyMessageIdAllocated(message, messageId);
1159 }
1160 }
1161
1162 public long getMessageId() {
1163 synchronized(mDraftIdLock) {
1164 if (mDraftId == UIProvider.INVALID_MESSAGE_ID) {
1165 // We don't have the message Id, check to see if we have a restored
1166 // request id, and see if we have a message for that request.
1167 if (mRestoredRequestId != 0) {
1168 Long retrievedMessageId =
1169 sRequestMessageIdMap.get(mRestoredRequestId);
1170 if (retrievedMessageId != null) {
1171 mDraftId = retrievedMessageId.longValue();
1172 }
1173 }
1174 }
1175 return mDraftId;
1176 }
1177 }
1178
1179 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1180 if (success) {
1181 // Successfully sent or saved so reset change markers
1182 discardChanges();
1183 } else {
1184 // A failure happened with saving/sending the draft
1185 // TODO(pwestbro): add a better string that should be used when failing to
1186 // send or save
1187 Toast.makeText(ComposeActivity.this, R.string.send_failed,
1188 Toast.LENGTH_SHORT).show();
1189 }
1190
1191 int numTasks;
1192 synchronized(mActiveTasks) {
1193 // Remove the task from the list of active tasks
1194 mActiveTasks.remove(task);
1195 numTasks = mActiveTasks.size();
1196 }
1197
1198 if (numTasks == 0) {
1199 // Stop service so we can be killed.
1200 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1201 }
1202 if (sTestSendOrSaveCallback != null) {
1203 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1204 }
1205 }
1206 };
1207
1208 // Get the selected account if the from spinner has been setup.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001209 Account selectedAccount = mAccount;
1210 String fromAddress = selectedAccount.name;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001211 if (selectedAccount == null || fromAddress == null) {
1212 // We don't have either the selected account or from address,
1213 // use mAccount.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001214 selectedAccount = mAccount;
1215 fromAddress = mAccount.name;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001216 }
1217
1218 if (mSendSaveTaskHandler == null) {
1219 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1220 handlerThread.start();
1221
1222 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1223 }
1224
1225 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body,
1226 to, cc, bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedText(),
1227 mAttachmentsView.getAttachments(), mRefMessageId, callback, mSendSaveTaskHandler,
1228 save, mForward);
1229
1230 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1231 mRecipient = selectedAccount.name;
1232 }
1233 mAccount = selectedAccount;
1234
1235 // Don't display the toast if the user is just changing the orientation, but we still
1236 // need to save the draft to the cursor because this is how we restore the attachments
1237 // when the configuration change completes.
1238 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1239 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1240 Toast.LENGTH_LONG).show();
1241 }
1242
1243 // Need to update variables here
1244 // because the send or save completes asynchronously even though the
1245 // toast shows right away.
1246 discardChanges();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001247 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001248
1249 // If we are sending, finish the activity
1250 if (!save) {
1251 finish();
1252 }
1253 }
1254
1255 /**
1256 * Save the state of the request messageid map. This allows for the Gmail process
1257 * to be killed, but and still allow for ComposeActivity instances to be recreated
1258 * correctly.
1259 */
1260 private void saveRequestMap() {
1261 // TODO: store the request map in user preferences.
1262 }
1263
Mindy Pereira013194c2012-01-06 15:09:33 -08001264 public void doAttach() {
1265 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1266 i.addCategory(Intent.CATEGORY_OPENABLE);
1267 if (Settings.System.getInt(
1268 getContentResolver(), UIProvider.getAttachmentTypeSetting(), 0) != 0) {
1269 i.setType("*/*");
1270 } else {
1271 i.setType("image/*");
1272 }
1273 mAddingAttachment = true;
1274 startActivityForResult(Intent.createChooser(i,
1275 getText(R.string.select_attachment_type)), RESULT_PICK_ATTACHMENT);
1276 }
1277
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001278 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001279 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001280 if (mCcBccButton != null) {
1281 mCcBccButton.setVisibility(View.GONE);
1282 }
1283 }
1284
Mindy Pereira326c6602012-01-04 15:32:42 -08001285 @Override
1286 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001287 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001288 if (position == ComposeActivity.REPLY) {
1289 mComposeMode = ComposeActivity.REPLY;
1290 } else if (position == ComposeActivity.REPLY_ALL) {
1291 mComposeMode = ComposeActivity.REPLY_ALL;
1292 } else if (position == ComposeActivity.FORWARD) {
1293 mComposeMode = ComposeActivity.FORWARD;
1294 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001295 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001296 resetMessageForModeChange();
Mindy Pereira33fe9082012-01-09 16:24:30 -08001297 initFromRefMessage(mComposeMode, mAccount.name);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001298 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001299 return true;
1300 }
1301
Mindy Pereira154386a2012-01-11 13:02:33 -08001302 private void resetMessageForModeChange() {
1303 // When switching between reply, reply all, forward,
1304 // follow the behavior of webview.
1305 // The contents of the following fields are cleared
1306 // so that they can be populated directly from the
1307 // ref message:
1308 // 1) Any recipient fields
1309 // 2) The subject
1310 mTo.setText("");
1311 mCc.setText("");
1312 mBcc.setText("");
1313 // Any edits to the subject are replaced with the original subject.
1314 mSubject.setText("");
1315
1316 // Any changes to the contents of the following fields are kept:
1317 // 1) Body
1318 // 2) Attachments
1319 // If the user made changes to attachments, keep their changes.
1320 if (!mAttachmentsChanged) {
1321 mAttachmentsView.deleteAllAttachments();
1322 }
1323 }
1324
Mindy Pereira326c6602012-01-04 15:32:42 -08001325 private class ComposeModeAdapter extends ArrayAdapter<String> {
1326
1327 private LayoutInflater mInflater;
1328
1329 public ComposeModeAdapter(Context context) {
1330 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1331 .getStringArray(R.array.compose_modes));
1332 }
1333
1334 private LayoutInflater getInflater() {
1335 if (mInflater == null) {
1336 mInflater = LayoutInflater.from(getContext());
1337 }
1338 return mInflater;
1339 }
1340
1341 @Override
1342 public View getView(int position, View convertView, ViewGroup parent) {
1343 if (convertView == null) {
1344 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1345 }
1346 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1347 return super.getView(position, convertView, parent);
1348 }
1349 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001350
1351 @Override
1352 public void onRespondInline(String text) {
1353 appendToBody(text, false);
1354 }
1355
1356 /**
1357 * Append text to the body of the message. If there is no existing body
1358 * text, just sets the body to text.
1359 *
1360 * @param text
1361 * @param withSignature True to append a signature.
1362 */
1363 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001364 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001365 if (bodyText != null && bodyText.length() > 0) {
1366 bodyText.append(text);
1367 } else {
1368 setBody(text, withSignature);
1369 }
1370 }
1371
1372 /**
1373 * Set the body of the message.
1374 * @param text
1375 * @param withSignature True to append a signature.
1376 */
1377 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001378 mBodyView.setText(text);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001379 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001380
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001381 @Override
1382 public void onAccountChanged() {
1383 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001384 mAccount = selectedAccountInfo;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001385
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001386 // TODO: handle discarding attachments when switching accounts.
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001387 // Only enable save for this draft if there is any other content
1388 // in the message.
1389 if (!isBlank()) {
1390 enableSave(true);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001391 }
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001392 mReplyFromChanged = true;
Mindy Pereira818143e2012-01-11 13:59:49 -08001393 initRecipients();
Mindy Pereira1a95a572012-01-05 12:21:29 -08001394 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001395
1396 public void enableSave(boolean enabled) {
1397 if (mSave != null) {
1398 mSave.setEnabled(enabled);
1399 }
1400 }
1401
1402 public void enableSend(boolean enabled) {
1403 if (mSend != null) {
1404 mSend.setEnabled(enabled);
1405 }
1406 }
1407
1408 /**
1409 * Handles button clicks from any error dialogs dealing with sending
1410 * a message.
1411 */
1412 @Override
1413 public void onClick(DialogInterface dialog, int which) {
1414 switch (which) {
1415 case DialogInterface.BUTTON_POSITIVE: {
1416 doDiscardWithoutConfirmation(true /* show toast */ );
1417 break;
1418 }
1419 case DialogInterface.BUTTON_NEGATIVE: {
1420 // If the user cancels the send, re-enable the send button.
1421 enableSend(true);
1422 break;
1423 }
1424 }
1425
1426 }
1427
1428 /**
1429 * Effectively discard the current message.
1430 *
1431 * This method is either invoked from the menu or from the dialog
1432 * once the user has confirmed that they want to discard the message.
1433 * @param showToast show "Message discarded" toast if true
1434 */
1435 private void doDiscardWithoutConfirmation(boolean showToast) {
1436 synchronized (mDraftIdLock) {
1437 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1438 ContentValues values = new ContentValues();
1439 values.put(MessageColumns.SERVER_ID, mDraftId);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001440 getContentResolver().update(Uri.parse(mAccount.expungeMessageUri),
Mindy Pereira82cc5662012-01-09 17:29:30 -08001441 values, null, null);
1442 // This is not strictly necessary (since we should not try to
1443 // save the draft after calling this) but it ensures that if we
1444 // do save again for some reason we make a new draft rather than
1445 // trying to resave an expunged draft.
1446 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1447 }
1448 }
1449
1450 if (showToast) {
1451 // Display a toast to let the user know
1452 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1453 }
1454
1455 // This prevents the draft from being saved in onPause().
1456 discardChanges();
1457 finish();
1458 }
1459
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001460 private void saveIfNeeded() {
1461 if (mAccount == null) {
1462 // We have not chosen an account yet so there's no way that we can save. This is ok,
1463 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1464 // user has not interacted with us yet and there is no real state to save.
1465 return;
1466 }
1467
1468 if (shouldSave()) {
1469 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1470 }
1471 }
1472
1473 private void saveIfNeededOnOrientationChanged() {
1474 if (mAccount == null) {
1475 // We have not chosen an account yet so there's no way that we can save. This is ok,
1476 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1477 // user has not interacted with us yet and there is no real state to save.
1478 return;
1479 }
1480
1481 if (shouldSave()) {
1482 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1483 }
1484 }
1485
1486 /**
1487 * Save a draft if a draft already exists or the message is not empty.
1488 */
1489 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1490 saveOnOrientationChanged();
1491 if (resetIME) {
1492 // Clear the IME composing suggestions from the body.
1493 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1494 }
1495 }
1496
1497 protected boolean saveOnOrientationChanged() {
1498 return sendOrSaveWithSanityChecks(true, false, true);
1499 }
1500
1501 @Override
1502 public void onAttachmentDeleted() {
1503 mAttachmentsChanged = true;
1504 updateSaveUi();
1505 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001506
1507
1508 /**
1509 * This is called any time one of our text fields changes.
1510 */
1511 public void afterTextChanged(Editable s) {
1512 mTextChanged = true;
1513 updateSaveUi();
1514 }
1515
1516 @Override
1517 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1518 // Do nothing.
1519 }
1520
1521 public void onTextChanged(CharSequence s, int start, int before, int count) {
1522 // Do nothing.
1523 }
1524
1525
1526 // There is a big difference between the text associated with an address changing
1527 // to add the display name or to format properly and a recipient being added or deleted.
1528 // Make sure we only notify of changes when a recipient has been added or deleted.
1529 private class RecipientTextWatcher implements TextWatcher {
1530 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
1531
1532 private RecipientEditTextView mView;
1533
1534 private TextWatcher mListener;
1535
1536 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
1537 mView = view;
1538 mListener = listener;
1539 }
1540
1541 @Override
1542 public void afterTextChanged(Editable s) {
1543 if (hasChanged()) {
1544 mListener.afterTextChanged(s);
1545 }
1546 }
1547
1548 private boolean hasChanged() {
1549 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
1550 int totalCount = currRecips.length;
1551 int totalPrevCount = 0;
1552 for (Entry<String, Integer> entry : mContent.entrySet()) {
1553 totalPrevCount += entry.getValue();
1554 }
1555 if (totalCount != totalPrevCount) {
1556 return true;
1557 }
1558
1559 for (String recip : currRecips) {
1560 if (!mContent.containsKey(recip)) {
1561 return true;
1562 } else {
1563 int count = mContent.get(recip) - 1;
1564 if (count < 0) {
1565 return true;
1566 } else {
1567 mContent.put(recip, count);
1568 }
1569 }
1570 }
1571 return false;
1572 }
1573
1574 private String[] tokenizeRecips(String[] recips) {
1575 // Tokenize them all and put them in the list.
1576 String[] recipAddresses = new String[recips.length];
1577 for (int i = 0; i < recips.length; i++) {
1578 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
1579 }
1580 return recipAddresses;
1581 }
1582
1583 @Override
1584 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1585 String[] recips = tokenizeRecips(getAddressesFromList(mView));
1586 for (String recip : recips) {
1587 if (!mContent.containsKey(recip)) {
1588 mContent.put(recip, 1);
1589 } else {
1590 mContent.put(recip, (mContent.get(recip)) + 1);
1591 }
1592 }
1593 }
1594
1595 @Override
1596 public void onTextChanged(CharSequence s, int start, int before, int count) {
1597 // Do nothing.
1598 }
1599 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001600}