blob: d49ddcb31263a9cd12ca85b9381fde884411b91d [file] [log] [blame]
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Mindy Pereira326c6602012-01-04 15:32:42 -080019import android.app.ActionBar;
Mindy Pereira82cc5662012-01-09 17:29:30 -080020import android.app.ActivityManager;
21import android.app.AlertDialog;
22import android.app.Dialog;
Mindy Pereira326c6602012-01-04 15:32:42 -080023import android.app.ActionBar.OnNavigationListener;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080024import android.app.Activity;
Mindy Pereira181df782012-03-01 13:32:44 -080025import android.app.LoaderManager.LoaderCallbacks;
Mindy Pereira6349a042012-01-04 11:25:01 -080026import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080027import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080028import android.content.Context;
Mindy Pereira181df782012-03-01 13:32:44 -080029import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080030import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080031import android.content.Intent;
Mindy Pereira181df782012-03-01 13:32:44 -080032import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080033import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080034import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080035import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080036import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080037import android.os.Handler;
38import android.os.HandlerThread;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080040import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080041import android.text.Html;
42import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080043import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080044import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080045import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080046import android.text.util.Rfc822Tokenizer;
Mindy Pereira326c6602012-01-04 15:32:42 -080047import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080048import android.view.Menu;
49import android.view.MenuInflater;
50import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080051import android.view.View;
Mindy Pereira326c6602012-01-04 15:32:42 -080052import android.view.ViewGroup;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080053import android.view.View.OnClickListener;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080054import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080055import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080056import android.widget.Button;
Mindy Pereira6349a042012-01-04 11:25:01 -080057import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080058import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080059
Mindy Pereirac17d0732011-12-29 10:46:19 -080060import com.android.common.Rfc822Validator;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080061import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080062import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080063import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080064import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080065import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080066import com.android.mail.providers.Address;
67import com.android.mail.providers.Attachment;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080068import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080069import com.android.mail.providers.MessageModification;
Mindy Pereira181df782012-03-01 13:32:44 -080070import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080071import com.android.mail.providers.UIProvider;
Mindy Pereira82cc5662012-01-09 17:29:30 -080072import com.android.mail.providers.UIProvider.MessageColumns;
Andy Huang30e2c242012-01-06 18:14:30 -080073import com.android.mail.R;
Andy Huang30e2c242012-01-06 18:14:30 -080074import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080075import com.android.mail.utils.Utils;
Mindy Pereirac17d0732011-12-29 10:46:19 -080076import com.android.ex.chips.RecipientEditTextView;
Mindy Pereira181df782012-03-01 13:32:44 -080077import com.google.android.gm.persistence.Persistence;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080078import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080079import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080080import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080081
Mindy Pereira46ce0b12012-01-05 10:32:15 -080082import java.util.ArrayList;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080083import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080084import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080085import java.util.HashSet;
86import java.util.List;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080087import java.util.Set;
Mindy Pereira75f66632012-01-11 11:42:02 -080088import java.util.Map.Entry;
Mindy Pereira82cc5662012-01-09 17:29:30 -080089import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080090
91public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080092 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Mindy Pereira181df782012-03-01 13:32:44 -080093 AttachmentDeletedListener, OnAccountChangedListener, LoaderCallbacks<Cursor> {
Mindy Pereira6349a042012-01-04 11:25:01 -080094 // Identifiers for which type of composition this is
95 static final int COMPOSE = -1; // also used for editing a draft
96 static final int REPLY = 0;
97 static final int REPLY_ALL = 1;
98 static final int FORWARD = 2;
99
100 // Integer extra holding one of the above compose action
101 private static final String EXTRA_ACTION = "action";
102
Mindy Pereira82cc5662012-01-09 17:29:30 -0800103 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
104 // Map containing information about requests to create new messages, and the id of the
105 // messages that were the result of those requests.
106 //
107 // This map is used when the activity that initiated the save a of a new message, is killed
108 // before the save has completed (and when we know the id of the newly created message). When
109 // a save is completed, the service that is running in the background, will update the map
110 //
111 // When a new ComposeActivity instance is created, it will attempt to use the information in
112 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
113 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
114 // to populate the map with data stored in shared preferences.
115 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
116 // Key used to store the above map
117 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800118 /**
119 * Notifies the {@code Activity} that the caller is an Email
120 * {@code Activity}, so that the back behavior may be modified accordingly.
121 *
122 * @see #onAppUpPressed
123 */
124 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
125
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800126 // If this is a reply/forward then this extra will hold the original message
127 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800128 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800129 private static final String LOG_TAG = new LogUtils().getLogTag();
130 // Request numbers for activities we start
131 private static final int RESULT_PICK_ATTACHMENT = 1;
132 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira181df782012-03-01 13:32:44 -0800133 private static final int ACCOUNT_SETTINGS_LOADER = 0;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800134
Mindy Pereira82cc5662012-01-09 17:29:30 -0800135 /**
136 * A single thread for running tasks in the background.
137 */
138 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800139 private RecipientEditTextView mTo;
140 private RecipientEditTextView mCc;
141 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800142 private Button mCcBccButton;
143 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800144 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800145 private Account mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800146 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800147 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800148 private TextView mSubject;
149
Mindy Pereira326c6602012-01-04 15:32:42 -0800150 private ComposeModeAdapter mComposeModeAdapter;
151 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800152 private boolean mForward;
153 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800154 private QuotedTextView mQuotedTextView;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800155 private TextView mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800156 private View mFromStatic;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800157 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800158 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800159 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800160 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800161 private boolean mTextChanged;
162 private boolean mReplyFromChanged;
163 private MenuItem mSave;
164 private MenuItem mSend;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800165 private String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800166 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800167 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800168 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800169 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
170 private Message mDraft;
171 private Object mDraftLock = new Object();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800172
Mindy Pereira326c6602012-01-04 15:32:42 -0800173 /**
174 * Can be called from a non-UI thread.
175 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800176 public static void editDraft(Context launcher, Account account, Message message) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800177 }
178
Mindy Pereira6349a042012-01-04 11:25:01 -0800179 /**
180 * Can be called from a non-UI thread.
181 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800182 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800183 launch(launcher, account, null, COMPOSE);
184 }
185
186 /**
187 * Can be called from a non-UI thread.
188 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800189 public static void reply(Context launcher, Account account, Message message) {
190 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800191 }
192
193 /**
194 * Can be called from a non-UI thread.
195 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800196 public static void replyAll(Context launcher, Account account, Message message) {
197 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800198 }
199
200 /**
201 * Can be called from a non-UI thread.
202 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800203 public static void forward(Context launcher, Account account, Message message) {
204 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800205 }
206
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800207 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800208 Intent intent = new Intent(launcher, ComposeActivity.class);
209 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
210 intent.putExtra(EXTRA_ACTION, action);
211 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800212 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
Mindy Pereira6349a042012-01-04 11:25:01 -0800213 launcher.startActivity(intent);
214 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800215
216 @Override
217 public void onCreate(Bundle savedInstanceState) {
218 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800219 setContentView(R.layout.compose);
220 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800221 Intent intent = getIntent();
222 setAccount((Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT));
223 if (mAccount == null) {
224 return;
225 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800226 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800227 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereira29ef1b82012-01-13 11:26:21 -0800228 if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800229 initFromRefMessage(action, mAccount.name);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800230 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800231 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800232 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800233 initRecipients();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800234 initActionBar(action);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800235 initFromSpinner();
Mindy Pereira75f66632012-01-11 11:42:02 -0800236 initChangeListeners();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800237 }
238
239 @Override
240 protected void onResume() {
241 super.onResume();
242 // Update the from spinner as other accounts
243 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800244 if (mFromSpinner != null && mAccount != null) {
245 mFromSpinner.asyncInitFromSpinner();
246 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800247 }
248
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800249 @Override
250 protected void onPause() {
251 super.onPause();
252
253 if (mSendConfirmDialog != null) {
254 mSendConfirmDialog.dismiss();
255 }
256 if (mRecipientErrorDialog != null) {
257 mRecipientErrorDialog.dismiss();
258 }
259
260 saveIfNeeded();
261 }
262
263 @Override
264 protected final void onActivityResult(int request, int result, Intent data) {
265 mAddingAttachment = false;
266
267 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
268 addAttachmentAndUpdateView(data);
269 }
270 }
271
272 @Override
273 public final void onSaveInstanceState(Bundle state) {
274 super.onSaveInstanceState(state);
275
276 // onSaveInstanceState is only called if the user might come back to this activity so it is
277 // not an ideal location to save the draft. However, if we have never saved the draft before
278 // we have to save it here in order to have an id to save in the bundle.
279 saveIfNeededOnOrientationChanged();
280 }
281
Mindy Pereira818143e2012-01-11 13:59:49 -0800282 @VisibleForTesting
283 void setAccount(Account account) {
284 mAccount = account;
Mindy Pereira181df782012-03-01 13:32:44 -0800285 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800286 }
287
Mindy Pereira1a95a572012-01-05 12:21:29 -0800288 private void initFromSpinner() {
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800289 mFromSpinner.setCurrentAccount(mAccount);
290 mFromSpinner.asyncInitFromSpinner();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800291 boolean showSpinner = mFromSpinner.getCount() > 1;
292 // If there is only 1 account, just show that account.
293 // Otherwise, give the user the ability to choose which account to send
294 // mail from / save drafts to.
295 mFromStatic.setVisibility(
296 showSpinner ? View.GONE : View.VISIBLE);
297 mFromSpinnerWrapper.setVisibility(
298 showSpinner ? View.VISIBLE : View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800299 }
300
301 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800302 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800303 if (mCcBccButton != null) {
304 mCcBccButton.setOnClickListener(this);
305 }
306 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800307 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira818143e2012-01-11 13:59:49 -0800308 mTo = (RecipientEditTextView) findViewById(R.id.to);
309 mCc = (RecipientEditTextView) findViewById(R.id.cc);
310 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800311 // TODO: add special chips text change watchers before adding
312 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800313 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800314 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
315 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800316 mBodyView = (TextView) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800317 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800318 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800319 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800320 }
321
Mindy Pereira75f66632012-01-11 11:42:02 -0800322 // Now that the message has been initialized from any existing draft or
323 // ref message data, set up listeners for any changes that occur to the
324 // message.
325 private void initChangeListeners() {
326 mSubject.addTextChangedListener(this);
327 mBodyView.addTextChangedListener(this);
328 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
329 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
330 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
331 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800332 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800333 }
334
Mindy Pereira326c6602012-01-04 15:32:42 -0800335 private void initActionBar(int action) {
336 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800337 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800338 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800339 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
340 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800341 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800342 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800343 if (mComposeModeAdapter == null) {
344 mComposeModeAdapter = new ComposeModeAdapter(this);
345 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800346 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
347 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800348 switch (action) {
349 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800350 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800351 break;
352 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800353 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800354 break;
355 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800356 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800357 break;
358 }
359 }
360 }
361
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800362 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800363 if (mRefMessage != null) {
364 mRefMessageId = mRefMessage.refMessageId;
365 setSubject(mRefMessage, action);
366 // Setup recipients
367 if (action == FORWARD) {
368 mForward = true;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800369 }
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800370 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
371 initBodyFromRefMessage(mRefMessage, action);
372 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
373 initAttachments(mRefMessage);
374 }
375 updateHideOrShowCcBcc();
Mindy Pereira6349a042012-01-04 11:25:01 -0800376 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800377 }
378
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800379 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800380 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800381 }
382
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800383 private void initBodyFromRefMessage(Message refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800384 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800385 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
386 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800387 }
388
389 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800390 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800391 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
392 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
393 if (ccVisible || bccVisible) {
394 mCcBccView.show(false, ccVisible, bccVisible);
395 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800396 if (mCcBccButton != null) {
397 if (!mCc.isShown() || !mBcc.isShown()) {
398 mCcBccButton.setVisibility(View.VISIBLE);
399 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
400 : R.string.add_bcc_label));
401 } else {
402 mCcBccButton.setVisibility(View.GONE);
403 }
404 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800405 }
406
Mindy Pereira013194c2012-01-06 15:09:33 -0800407 /**
408 * Add attachment and update the compose area appropriately.
409 * @param data
410 */
411 public void addAttachmentAndUpdateView(Intent data) {
412 Uri uri = data != null ? data.getData() : null;
Mindy Pereira013194c2012-01-06 15:09:33 -0800413 try {
Mindy Pereiraf944e962012-01-17 11:43:36 -0800414 long size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
415 true /* local file */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800416 if (size > 0) {
417 mAttachmentsChanged = true;
418 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800419 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800420 } catch (AttachmentFailureException e) {
421 // A toast has already been shown to the user, no need to do
422 // anything.
423 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800424 }
425 }
426
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800427 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800428 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800429 // Don't populate the address if this is a forward.
430 if (action == ComposeActivity.FORWARD) {
431 return;
432 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800433 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800434 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800435
Mindy Pereira818143e2012-01-11 13:59:49 -0800436 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800437 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800438 // This is the email address of the current user, i.e. the one composing
439 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800440 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800441 String fromAddress = refMessage.from;
442 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
443 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800444 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800445
446 // If this is a reply, the Cc list is empty. If this is a reply-all, the
447 // Cc list is the union of the To and Cc recipients of the original
448 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800449 // already on the To list.
450 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800451 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
452 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800453 addToAddresses(toAddresses);
454 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800455 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800456 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
457 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800458 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800459 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800460 addRecipients(accountEmail, ccAddresses,
461 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800462 addCcAddresses(ccAddresses, toAddresses);
463 }
464 }
465
466 private void addToAddresses(Collection<String> addresses) {
467 addAddressesToList(addresses, mTo);
468 }
469
470 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
471 addCcAddressesToList(tokenizeAddressList(addresses), tokenizeAddressList(toAddresses),
472 mCc);
473 }
474
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800475 @VisibleForTesting
476 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
477 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
478 String address;
479
480 HashSet<String> compareTo = convertToHashSet(compareToList);
481 for (Rfc822Token[] tokens : addresses) {
482 for (int i = 0; i < tokens.length; i++) {
483 address = tokens[i].toString();
484 // Check if this is a duplicate:
485 if (!compareTo.contains(tokens[i].getAddress())) {
486 // Get the address here
487 list.append(address + END_TOKEN);
488 }
489 }
490 }
491 }
492
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800493 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
494 HashSet<String> hash = new HashSet<String>();
495 for (Rfc822Token[] tokens : list) {
496 for (int i = 0; i < tokens.length; i++) {
497 hash.add(tokens[i].getAddress());
498 }
499 }
500 return hash;
501 }
502
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800503 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
504 @VisibleForTesting
505 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
506
507 for (String address: addresses) {
508 tokenized.add(Rfc822Tokenizer.tokenize(address));
509 }
510 return tokenized;
511 }
512
513 @VisibleForTesting
514 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
515 for (String address : addresses) {
516 addAddressToList(address, list);
517 }
518 }
519
520 private void addAddressToList(String address, RecipientEditTextView list) {
521 if (address == null || list == null)
522 return;
523
524 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
525
526 for (int i = 0; i < tokens.length; i++) {
527 list.append(tokens[i] + END_TOKEN);
528 }
529 }
530
531 @VisibleForTesting
532 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800533 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800534 // The To recipient is the reply-to address specified in the original
535 // message, unless it is:
536 // the current user OR a custom from of the current user, in which case
537 // it's the To recipient list of the original message.
538 // OR missing, in which case use the sender of the original message
539 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800540 if (!TextUtils.isEmpty(replyToAddress)) {
541 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800542 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800543 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800544 }
545 return toAddresses;
546 }
547
548 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
549 for (String email : addresses) {
550 // Do not add this account, or any of the custom froms, to the list
551 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800552 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800553 if (!account.equalsIgnoreCase(recipientAddress)) {
554 recipients.add(email.replace("\"\"", ""));
555 }
556 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800557 }
558
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800559 private void setSubject(Message refMessage, int action) {
560 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800561 String prefix;
562 String correctedSubject = null;
563 if (action == ComposeActivity.COMPOSE) {
564 prefix = "";
565 } else if (action == ComposeActivity.FORWARD) {
566 prefix = getString(R.string.forward_subject_label);
567 } else {
568 prefix = getString(R.string.reply_subject_label);
569 }
570
571 // Don't duplicate the prefix
572 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
573 correctedSubject = subject;
574 } else {
575 correctedSubject = String
576 .format(getString(R.string.formatted_subject), prefix, subject);
577 }
578 mSubject.setText(correctedSubject);
579 }
580
Mindy Pereira818143e2012-01-11 13:59:49 -0800581 private void initRecipients() {
582 setupRecipients(mTo);
583 setupRecipients(mCc);
584 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800585 }
586
Mindy Pereira818143e2012-01-11 13:59:49 -0800587 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800588 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800589 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800590 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800591 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800592 int offset = accountName.indexOf("@") + 1;
593 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800594 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800595 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800596 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800597 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800598 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800599 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800600 }
601
602 @Override
603 public void onClick(View v) {
604 int id = v.getId();
605 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800606 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800607 // Verify that cc/ bcc aren't showing.
608 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800609 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800610 break;
611 }
612 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800613
614 @Override
615 public boolean onCreateOptionsMenu(Menu menu) {
616 super.onCreateOptionsMenu(menu);
617 MenuInflater inflater = getMenuInflater();
618 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800619 mSave = menu.findItem(R.id.save);
620 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800621 return true;
622 }
623
624 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800625 public boolean onPrepareOptionsMenu(Menu menu) {
626 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -0800627 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800628 // Its possible there is a menu item OR a button.
629 boolean ccFieldVisible = mCc.isShown();
630 boolean bccFieldVisible = mBcc.isShown();
631 if (!ccFieldVisible || !bccFieldVisible) {
632 ccBcc.setVisible(true);
633 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
634 : R.string.add_bcc_label));
635 } else {
636 ccBcc.setVisible(false);
637 }
638 }
Mindy Pereira75f66632012-01-11 11:42:02 -0800639 if (mSave != null) {
640 mSave.setEnabled(shouldSave());
641 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800642 return true;
643 }
644
645 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800646 public boolean onOptionsItemSelected(MenuItem item) {
647 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -0800648 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800649 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -0800650 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -0800651 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -0800652 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800653 case R.id.add_cc_bcc:
654 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800655 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800656 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800657 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800658 break;
659 case R.id.send:
660 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -0800661 break;
662 default:
663 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800664 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800665 }
666 return !handled ? super.onOptionsItemSelected(item) : handled;
667 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800668
Mindy Pereira33fe9082012-01-09 16:24:30 -0800669 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800670 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800671 }
672
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800673 private void doSave(boolean showToast, boolean resetIME) {
674 sendOrSaveWithSanityChecks(true, showToast, false);
675 if (resetIME) {
676 // Clear the IME composing suggestions from the body.
677 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
678 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800679 }
680
Mindy Pereira82cc5662012-01-09 17:29:30 -0800681 /*package*/ interface SendOrSaveCallback {
682 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800683 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
684 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800685 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
686 }
687
688 /*package*/ static class SendOrSaveTask implements Runnable {
689 private final Context mContext;
690 private final SendOrSaveCallback mSendOrSaveCallback;
691 @VisibleForTesting
692 final SendOrSaveMessage mSendOrSaveMessage;
693
694 public SendOrSaveTask(Context context, SendOrSaveMessage message,
695 SendOrSaveCallback callback) {
696 mContext = context;
697 mSendOrSaveCallback = callback;
698 mSendOrSaveMessage = message;
699 }
700
701 @Override
702 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800703 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800704
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800705 final Account selectedAccount = sendOrSaveMessage.mSelectedAccount;
706 Message message = mSendOrSaveCallback.getMessage();
707 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800708 // If a previous draft has been saved, in an account that is different
709 // than what the user wants to send from, remove the old draft, and treat this
710 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800711 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800712 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
713 ContentResolver resolver = mContext.getContentResolver();
714 ContentValues values = new ContentValues();
715 values.put(BaseColumns._ID, messageId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -0800716 if (selectedAccount.expungeMessageUri != null) {
717 resolver.update(selectedAccount.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800718 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -0800719 } else {
720 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800721 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800722 // reset messageId to 0, so a new message will be created
723 messageId = UIProvider.INVALID_MESSAGE_ID;
724 }
725 }
726
727 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800728 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800729 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800730 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800731 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
732 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800733 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800734 ContentResolver resolver = mContext.getContentResolver();
735 Uri messageUri = resolver.insert(
Mindy Pereiracfb7f332012-02-28 10:23:43 -0800736 sendOrSaveMessage.mSave ? selectedAccount.saveDraftUri
737 : selectedAccount.sendMessageUri, sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800738 if (sendOrSaveMessage.mSave && messageUri != null) {
739 Cursor messageCursor = resolver.query(messageUri,
740 UIProvider.MESSAGE_PROJECTION, null, null, null);
741 if (messageCursor != null && messageCursor.moveToFirst()) {
742 // Broadcast notification that a new message has
743 // been allocated
744 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
Mindy Pereiraa831b2f2012-02-23 19:24:40 -0800745 new Message(messageCursor));
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800746 }
747 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800748 }
749
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800750 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800751 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800752 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -0800753 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800754 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -0800755 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800756 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -0800757 }
758 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
759 }
760 }
761
762 // Array of the outstanding send or save tasks. Access is synchronized
763 // with the object itself
764 /* package for testing */
765 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
766 private int mRequestId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800767
768 /*package*/ static class SendOrSaveMessage {
769 final Account mAccount;
770 final Account mSelectedAccount;
771 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800772 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800773 final boolean mSave;
774 final int mRequestId;
775
776 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800777 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800778 mAccount = account;
779 mSelectedAccount = selectedAccount;
780 mValues = values;
781 mRefMessageId = refMessageId;
782 mSave = save;
783 mRequestId = mValues.hashCode() ^ hashCode();
784 }
785
786 int requestId() {
787 return mRequestId;
788 }
789 }
790
791 /**
792 * Get the to recipients.
793 */
794 public String[] getToAddresses() {
795 return getAddressesFromList(mTo);
796 }
797
798 /**
799 * Get the cc recipients.
800 */
801 public String[] getCcAddresses() {
802 return getAddressesFromList(mCc);
803 }
804
805 /**
806 * Get the bcc recipients.
807 */
808 public String[] getBccAddresses() {
809 return getAddressesFromList(mBcc);
810 }
811
812 public String[] getAddressesFromList(RecipientEditTextView list) {
813 if (list == null) {
814 return new String[0];
815 }
816 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
817 int count = tokens.length;
818 String[] result = new String[count];
819 for (int i = 0; i < count; i++) {
820 result[i] = tokens[i].toString();
821 }
822 return result;
823 }
824
825 /**
826 * Check for invalid email addresses.
827 * @param to String array of email addresses to check.
828 * @param wrongEmailsOut Emails addresses that were invalid.
829 */
830 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
831 for (String email : to) {
832 if (!mValidator.isValid(email)) {
833 wrongEmailsOut.add(email);
834 }
835 }
836 }
837
838 /**
839 * Show an error because the user has entered an invalid recipient.
840 * @param message
841 */
842 public void showRecipientErrorDialog(String message) {
843 // Only 1 invalid recipients error dialog should be allowed up at a
844 // time.
845 if (mRecipientErrorDialog != null) {
846 mRecipientErrorDialog.dismiss();
847 }
848 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
849 R.string.recipient_error_dialog_title)
850 .setIconAttribute(android.R.attr.alertDialogIcon)
851 .setCancelable(false)
852 .setPositiveButton(
853 R.string.ok, new Dialog.OnClickListener() {
854 public void onClick(DialogInterface dialog, int which) {
855 // after the user dismisses the recipient error
856 // dialog we want to make sure to refocus the
857 // recipient to field so they can fix the issue
858 // easily
859 if (mTo != null) {
860 mTo.requestFocus();
861 }
862 mRecipientErrorDialog = null;
863 }
864 }).show();
865 }
866
867 /**
868 * Update the state of the UI based on whether or not the current draft
869 * needs to be saved and the message is not empty.
870 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800871 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800872 if (mSave != null) {
873 mSave.setEnabled((shouldSave() && !isBlank()));
874 }
875 }
876
877 /**
878 * Returns true if we need to save the current draft.
879 */
880 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800881 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800882 // The message should only be saved if:
883 // It hasn't been sent AND
884 // Some text has been added to the message OR
885 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800886 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -0800887 (mReplyFromChanged && !isBlank()));
888 }
889 }
890
891 /**
892 * Check if the ComposeArea believes all fields are blank.
893 * @return boolean
894 */
895 public boolean isBlank() {
896 return mSubject.getText().length() == 0
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800897 && mBodyView.getText().length() == 0
Mindy Pereira82cc5662012-01-09 17:29:30 -0800898 && mTo.length() == 0
899 && mCc.length() == 0
900 && mBcc.length() == 0
901 && mAttachmentsView.getAttachments().size() == 0;
902 }
903
904 /**
905 * Allows any changes made by the user to be ignored. Called when the user
906 * decides to discard a draft.
907 */
908 private void discardChanges() {
909 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800910 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800911 mReplyFromChanged = false;
912 }
913
914 /**
Mindy Pereira181df782012-03-01 13:32:44 -0800915 * @param body
916 * @param save
917 * @param showToast
918 * @return Whether the send or save succeeded.
919 */
920 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
921 final boolean orientationChanged) {
922 String[] to, cc, bcc;
923 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800924
Mindy Pereira181df782012-03-01 13:32:44 -0800925 if (orientationChanged) {
926 to = cc = bcc = new String[0];
927 } else {
928 to = getToAddresses();
929 cc = getCcAddresses();
930 bcc = getBccAddresses();
931 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800932
Mindy Pereira181df782012-03-01 13:32:44 -0800933 // Don't let the user send to nobody (but it's okay to save a message
934 // with no recipients)
935 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
936 showRecipientErrorDialog(getString(R.string.recipient_needed));
937 return false;
938 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800939
Mindy Pereira181df782012-03-01 13:32:44 -0800940 List<String> wrongEmails = new ArrayList<String>();
941 if (!save) {
942 checkInvalidEmails(to, wrongEmails);
943 checkInvalidEmails(cc, wrongEmails);
944 checkInvalidEmails(bcc, wrongEmails);
945 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800946
Mindy Pereira181df782012-03-01 13:32:44 -0800947 // Don't let the user send an email with invalid recipients
948 if (wrongEmails.size() > 0) {
949 String errorText = String.format(getString(R.string.invalid_recipient),
950 wrongEmails.get(0));
951 showRecipientErrorDialog(errorText);
952 return false;
953 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800954
Mindy Pereira181df782012-03-01 13:32:44 -0800955 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
956 public void onClick(DialogInterface dialog, int which) {
957 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
958 }
959 };
Mindy Pereira82cc5662012-01-09 17:29:30 -0800960
Mindy Pereira181df782012-03-01 13:32:44 -0800961 // Show a warning before sending only if there are no attachments.
962 if (!save) {
963 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
964 boolean warnAboutEmptySubject = isSubjectEmpty();
965 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800966
Mindy Pereira181df782012-03-01 13:32:44 -0800967 // A warning about an empty body may not be warranted when
968 // forwarding mails, since a common use case is to forward
969 // quoted text and not append any more text.
970 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800971
Mindy Pereira181df782012-03-01 13:32:44 -0800972 // When we bring up a dialog warning the user about a send,
973 // assume that they accept sending the message. If they do not,
974 // the dialog listener is required to enable sending again.
975 if (warnAboutEmptySubject) {
976 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
977 return true;
978 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800979
Mindy Pereira181df782012-03-01 13:32:44 -0800980 if (warnAboutEmptyBody) {
981 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
982 return true;
983 }
984 }
985 // Ask for confirmation to send (if always required)
986 if (showSendConfirmation()) {
987 showSendConfirmDialog(R.string.confirm_send_message, listener);
988 return true;
989 }
990 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800991
Mindy Pereira181df782012-03-01 13:32:44 -0800992 sendOrSave(body, save, showToast, false);
993 return true;
994 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800995
Mindy Pereira181df782012-03-01 13:32:44 -0800996 /**
997 * Returns a boolean indicating whether warnings should be shown for empty
998 * subject and body fields
999 *
1000 * @return True if a warning should be shown for empty text fields
1001 */
1002 protected boolean showEmptyTextWarnings() {
1003 return mAttachmentsView.getAttachments().size() == 0;
1004 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001005
Mindy Pereira181df782012-03-01 13:32:44 -08001006 /**
1007 * Returns a boolean indicating whether the user should confirm each send
1008 *
1009 * @return True if a warning should be on each send
1010 */
1011 protected boolean showSendConfirmation() {
1012 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1013 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001014
Mindy Pereira181df782012-03-01 13:32:44 -08001015 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1016 if (mSendConfirmDialog != null) {
1017 mSendConfirmDialog.dismiss();
1018 mSendConfirmDialog = null;
1019 }
1020 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1021 .setTitle(R.string.confirm_send_title)
1022 .setIconAttribute(android.R.attr.alertDialogIcon)
1023 .setPositiveButton(R.string.send, listener)
1024 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1025 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001026
Mindy Pereira181df782012-03-01 13:32:44 -08001027 /**
1028 * Returns whether the ComposeArea believes there is any text in the body of
1029 * the composition. TODO: When ComposeArea controls the Body as well, add
1030 * that here.
1031 */
1032 public boolean isBodyEmpty() {
1033 return !mQuotedTextView.isTextIncluded();
1034 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001035
Mindy Pereira181df782012-03-01 13:32:44 -08001036 /**
1037 * Test to see if the subject is empty.
1038 *
1039 * @return boolean.
1040 */
1041 // TODO: this will likely go away when composeArea.focus() is implemented
1042 // after all the widget control is moved over.
1043 public boolean isSubjectEmpty() {
1044 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1045 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001046
Mindy Pereira181df782012-03-01 13:32:44 -08001047 /* package */
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001048 static int sendOrSaveInternal(Context context, final Account account,
1049 final Account selectedAccount, String fromAddress, final Spanned body,
1050 final String[] to, final String[] cc, final String[] bcc, final String subject,
1051 final CharSequence quotedText, final List<Attachment> attachments,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001052 final String refMessageId, SendOrSaveCallback callback, Handler handler, boolean save,
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001053 boolean forward) {
1054 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001055
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001056 MessageModification.putToAddresses(values, to);
1057 MessageModification.putCcAddresses(values, cc);
1058 MessageModification.putBccAddresses(values, bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001059
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001060 MessageModification.putSubject(values, subject);
1061 String htmlBody = Html.toHtml(body);
1062 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1063 StringBuilder fullBody = new StringBuilder(htmlBody);
1064 if (includeQuotedText) {
1065 if (forward) {
1066 // forwarded messages get full text in HTML from client
1067 fullBody.append(quotedText);
1068 MessageModification.putForward(values, forward);
1069 } else {
1070 // replies get full quoted text from server - HTMl gets
1071 // converted to text for now
1072 final String text = quotedText.toString();
1073 if (QuotedTextView.containsQuotedText(text)) {
1074 int pos = QuotedTextView.getQuotedTextOffset(text);
1075 fullBody.append(text.substring(0, pos));
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001076 MessageModification.putForward(values, forward);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001077 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001078 } else {
1079 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1080 // This shouldn't happen, but just use what we have,
1081 // and don't do server-side expansion
1082 fullBody.append(text);
1083 }
1084 }
1085 }
1086 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1087 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereiraf944e962012-01-17 11:43:36 -08001088 MessageModification.putAttachments(values, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001089
Mindy Pereira181df782012-03-01 13:32:44 -08001090 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1091 values, refMessageId, save);
1092 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001093
Mindy Pereira181df782012-03-01 13:32:44 -08001094 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001095
Mindy Pereira181df782012-03-01 13:32:44 -08001096 // Do the send/save action on the specified handler to avoid possible
1097 // ANRs
1098 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001099
Mindy Pereira181df782012-03-01 13:32:44 -08001100 return sendOrSaveMessage.requestId();
1101 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001102
Mindy Pereira181df782012-03-01 13:32:44 -08001103 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1104 boolean orientationChanged) {
1105 // Check if user is a monkey. Monkeys can compose and hit send
1106 // button but are not allowed to send anything off the device.
1107 if (!save && ActivityManager.isUserAMonkey()) {
1108 return;
1109 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001110
Mindy Pereira181df782012-03-01 13:32:44 -08001111 String[] to, cc, bcc;
1112 if (orientationChanged) {
1113 to = cc = bcc = new String[0];
1114 } else {
1115 to = getToAddresses();
1116 cc = getCcAddresses();
1117 bcc = getBccAddresses();
1118 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001119
Mindy Pereira181df782012-03-01 13:32:44 -08001120 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001121 private int mRestoredRequestId;
1122
1123 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08001124 synchronized (mActiveTasks) {
1125 int numTasks = mActiveTasks.size();
1126 if (numTasks == 0) {
1127 // Start service so we won't be killed if this app is
1128 // put in the background.
1129 startService(new Intent(ComposeActivity.this, EmptyService.class));
1130 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001131
Mindy Pereira181df782012-03-01 13:32:44 -08001132 mActiveTasks.add(sendOrSaveTask);
1133 }
1134 if (sTestSendOrSaveCallback != null) {
1135 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1136 }
1137 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001138
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001139 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1140 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08001141 synchronized (mDraftLock) {
1142 mDraftId = message.id;
1143 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001144 if (sRequestMessageIdMap != null) {
1145 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1146 }
Mindy Pereira181df782012-03-01 13:32:44 -08001147 // Cache request message map, in case the process is killed
1148 saveRequestMap();
1149 }
1150 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001151 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08001152 }
1153 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001154
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001155 public Message getMessage() {
1156 synchronized (mDraftLock) {
1157 return mDraft;
1158 }
1159 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001160
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001161 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1162 if (success) {
1163 // Successfully sent or saved so reset change markers
1164 discardChanges();
1165 } else {
1166 // A failure happened with saving/sending the draft
1167 // TODO(pwestbro): add a better string that should be used
1168 // when failing to send or save
1169 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1170 .show();
1171 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001172
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001173 int numTasks;
1174 synchronized (mActiveTasks) {
1175 // Remove the task from the list of active tasks
1176 mActiveTasks.remove(task);
1177 numTasks = mActiveTasks.size();
1178 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001179
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001180 if (numTasks == 0) {
1181 // Stop service so we can be killed.
1182 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1183 }
1184 if (sTestSendOrSaveCallback != null) {
1185 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1186 }
1187 }
Mindy Pereira181df782012-03-01 13:32:44 -08001188 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001189
Mindy Pereira181df782012-03-01 13:32:44 -08001190 // Get the selected account if the from spinner has been setup.
1191 Account selectedAccount = mAccount;
1192 String fromAddress = selectedAccount.name;
1193 if (selectedAccount == null || fromAddress == null) {
1194 // We don't have either the selected account or from address,
1195 // use mAccount.
1196 selectedAccount = mAccount;
1197 fromAddress = mAccount.name;
1198 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001199
Mindy Pereira181df782012-03-01 13:32:44 -08001200 if (mSendSaveTaskHandler == null) {
1201 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1202 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001203
Mindy Pereira181df782012-03-01 13:32:44 -08001204 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1205 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001206
Mindy Pereira181df782012-03-01 13:32:44 -08001207 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body, to, cc,
1208 bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedText(),
1209 mAttachmentsView.getAttachments(), mRefMessageId, callback, mSendSaveTaskHandler,
1210 save, mForward);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001211
Mindy Pereira181df782012-03-01 13:32:44 -08001212 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1213 mRecipient = selectedAccount.name;
1214 }
1215 mAccount = selectedAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001216
Mindy Pereira181df782012-03-01 13:32:44 -08001217 // Don't display the toast if the user is just changing the orientation,
1218 // but we still need to save the draft to the cursor because this is how we restore
1219 // the attachments when the configuration change completes.
1220 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1221 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1222 Toast.LENGTH_LONG).show();
1223 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001224
Mindy Pereira181df782012-03-01 13:32:44 -08001225 // Need to update variables here because the send or save completes
1226 // asynchronously even though the toast shows right away.
1227 discardChanges();
1228 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001229
Mindy Pereira181df782012-03-01 13:32:44 -08001230 // If we are sending, finish the activity
1231 if (!save) {
1232 finish();
1233 }
1234 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001235
Mindy Pereira181df782012-03-01 13:32:44 -08001236 /**
1237 * Save the state of the request messageid map. This allows for the Gmail
1238 * process to be killed, but and still allow for ComposeActivity instances
1239 * to be recreated correctly.
1240 */
1241 private void saveRequestMap() {
1242 // TODO: store the request map in user preferences.
1243 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001244
Mindy Pereira013194c2012-01-06 15:09:33 -08001245 public void doAttach() {
1246 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1247 i.addCategory(Intent.CATEGORY_OPENABLE);
Mindy Pereira181df782012-03-01 13:32:44 -08001248 if (android.provider.Settings.System.getInt(getContentResolver(),
1249 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08001250 i.setType("*/*");
1251 } else {
1252 i.setType("image/*");
1253 }
1254 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08001255 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
1256 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08001257 }
1258
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001259 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001260 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001261 if (mCcBccButton != null) {
1262 mCcBccButton.setVisibility(View.GONE);
1263 }
1264 }
1265
Mindy Pereira326c6602012-01-04 15:32:42 -08001266 @Override
1267 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001268 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001269 if (position == ComposeActivity.REPLY) {
1270 mComposeMode = ComposeActivity.REPLY;
1271 } else if (position == ComposeActivity.REPLY_ALL) {
1272 mComposeMode = ComposeActivity.REPLY_ALL;
1273 } else if (position == ComposeActivity.FORWARD) {
1274 mComposeMode = ComposeActivity.FORWARD;
1275 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001276 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001277 resetMessageForModeChange();
Mindy Pereira33fe9082012-01-09 16:24:30 -08001278 initFromRefMessage(mComposeMode, mAccount.name);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001279 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001280 return true;
1281 }
1282
Mindy Pereira154386a2012-01-11 13:02:33 -08001283 private void resetMessageForModeChange() {
1284 // When switching between reply, reply all, forward,
1285 // follow the behavior of webview.
1286 // The contents of the following fields are cleared
1287 // so that they can be populated directly from the
1288 // ref message:
1289 // 1) Any recipient fields
1290 // 2) The subject
1291 mTo.setText("");
1292 mCc.setText("");
1293 mBcc.setText("");
1294 // Any edits to the subject are replaced with the original subject.
1295 mSubject.setText("");
1296
1297 // Any changes to the contents of the following fields are kept:
1298 // 1) Body
1299 // 2) Attachments
1300 // If the user made changes to attachments, keep their changes.
1301 if (!mAttachmentsChanged) {
1302 mAttachmentsView.deleteAllAttachments();
1303 }
1304 }
1305
Mindy Pereira326c6602012-01-04 15:32:42 -08001306 private class ComposeModeAdapter extends ArrayAdapter<String> {
1307
1308 private LayoutInflater mInflater;
1309
1310 public ComposeModeAdapter(Context context) {
1311 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1312 .getStringArray(R.array.compose_modes));
1313 }
1314
1315 private LayoutInflater getInflater() {
1316 if (mInflater == null) {
1317 mInflater = LayoutInflater.from(getContext());
1318 }
1319 return mInflater;
1320 }
1321
1322 @Override
1323 public View getView(int position, View convertView, ViewGroup parent) {
1324 if (convertView == null) {
1325 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1326 }
1327 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1328 return super.getView(position, convertView, parent);
1329 }
1330 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001331
1332 @Override
1333 public void onRespondInline(String text) {
1334 appendToBody(text, false);
1335 }
1336
1337 /**
1338 * Append text to the body of the message. If there is no existing body
1339 * text, just sets the body to text.
1340 *
1341 * @param text
1342 * @param withSignature True to append a signature.
1343 */
1344 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001345 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001346 if (bodyText != null && bodyText.length() > 0) {
1347 bodyText.append(text);
1348 } else {
1349 setBody(text, withSignature);
1350 }
1351 }
1352
1353 /**
1354 * Set the body of the message.
1355 * @param text
1356 * @param withSignature True to append a signature.
1357 */
1358 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001359 mBodyView.setText(text);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001360 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001361
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001362 @Override
1363 public void onAccountChanged() {
1364 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereira181df782012-03-01 13:32:44 -08001365 if (!mAccount.equals(selectedAccountInfo)) {
1366 mAccount = selectedAccountInfo;
1367 mCachedSettings = null;
1368 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
1369 // TODO: handle discarding attachments when switching accounts.
1370 // Only enable save for this draft if there is any other content
1371 // in the message.
1372 if (!isBlank()) {
1373 enableSave(true);
1374 }
1375 mReplyFromChanged = true;
1376 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001377 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001378 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001379
1380 public void enableSave(boolean enabled) {
1381 if (mSave != null) {
1382 mSave.setEnabled(enabled);
1383 }
1384 }
1385
1386 public void enableSend(boolean enabled) {
1387 if (mSend != null) {
1388 mSend.setEnabled(enabled);
1389 }
1390 }
1391
1392 /**
1393 * Handles button clicks from any error dialogs dealing with sending
1394 * a message.
1395 */
1396 @Override
1397 public void onClick(DialogInterface dialog, int which) {
1398 switch (which) {
1399 case DialogInterface.BUTTON_POSITIVE: {
1400 doDiscardWithoutConfirmation(true /* show toast */ );
1401 break;
1402 }
1403 case DialogInterface.BUTTON_NEGATIVE: {
1404 // If the user cancels the send, re-enable the send button.
1405 enableSend(true);
1406 break;
1407 }
1408 }
1409
1410 }
1411
1412 /**
1413 * Effectively discard the current message.
1414 *
1415 * This method is either invoked from the menu or from the dialog
1416 * once the user has confirmed that they want to discard the message.
1417 * @param showToast show "Message discarded" toast if true
1418 */
1419 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001420 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001421 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1422 ContentValues values = new ContentValues();
1423 values.put(MessageColumns.SERVER_ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001424 if (mAccount.expungeMessageUri != null) {
1425 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
1426 } else {
1427 // TODO(mindyp): call delete on this conversation instead.
1428 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001429 // This is not strictly necessary (since we should not try to
1430 // save the draft after calling this) but it ensures that if we
1431 // do save again for some reason we make a new draft rather than
1432 // trying to resave an expunged draft.
1433 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1434 }
1435 }
1436
1437 if (showToast) {
1438 // Display a toast to let the user know
1439 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1440 }
1441
1442 // This prevents the draft from being saved in onPause().
1443 discardChanges();
1444 finish();
1445 }
1446
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001447 private void saveIfNeeded() {
1448 if (mAccount == null) {
1449 // We have not chosen an account yet so there's no way that we can save. This is ok,
1450 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1451 // user has not interacted with us yet and there is no real state to save.
1452 return;
1453 }
1454
1455 if (shouldSave()) {
1456 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1457 }
1458 }
1459
1460 private void saveIfNeededOnOrientationChanged() {
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 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1470 }
1471 }
1472
1473 /**
1474 * Save a draft if a draft already exists or the message is not empty.
1475 */
1476 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1477 saveOnOrientationChanged();
1478 if (resetIME) {
1479 // Clear the IME composing suggestions from the body.
1480 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1481 }
1482 }
1483
1484 protected boolean saveOnOrientationChanged() {
1485 return sendOrSaveWithSanityChecks(true, false, true);
1486 }
1487
1488 @Override
1489 public void onAttachmentDeleted() {
1490 mAttachmentsChanged = true;
1491 updateSaveUi();
1492 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001493
1494
1495 /**
1496 * This is called any time one of our text fields changes.
1497 */
1498 public void afterTextChanged(Editable s) {
1499 mTextChanged = true;
1500 updateSaveUi();
1501 }
1502
1503 @Override
1504 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1505 // Do nothing.
1506 }
1507
1508 public void onTextChanged(CharSequence s, int start, int before, int count) {
1509 // Do nothing.
1510 }
1511
1512
1513 // There is a big difference between the text associated with an address changing
1514 // to add the display name or to format properly and a recipient being added or deleted.
1515 // Make sure we only notify of changes when a recipient has been added or deleted.
1516 private class RecipientTextWatcher implements TextWatcher {
1517 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
1518
1519 private RecipientEditTextView mView;
1520
1521 private TextWatcher mListener;
1522
1523 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
1524 mView = view;
1525 mListener = listener;
1526 }
1527
1528 @Override
1529 public void afterTextChanged(Editable s) {
1530 if (hasChanged()) {
1531 mListener.afterTextChanged(s);
1532 }
1533 }
1534
1535 private boolean hasChanged() {
1536 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
1537 int totalCount = currRecips.length;
1538 int totalPrevCount = 0;
1539 for (Entry<String, Integer> entry : mContent.entrySet()) {
1540 totalPrevCount += entry.getValue();
1541 }
1542 if (totalCount != totalPrevCount) {
1543 return true;
1544 }
1545
1546 for (String recip : currRecips) {
1547 if (!mContent.containsKey(recip)) {
1548 return true;
1549 } else {
1550 int count = mContent.get(recip) - 1;
1551 if (count < 0) {
1552 return true;
1553 } else {
1554 mContent.put(recip, count);
1555 }
1556 }
1557 }
1558 return false;
1559 }
1560
1561 private String[] tokenizeRecips(String[] recips) {
1562 // Tokenize them all and put them in the list.
1563 String[] recipAddresses = new String[recips.length];
1564 for (int i = 0; i < recips.length; i++) {
1565 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
1566 }
1567 return recipAddresses;
1568 }
1569
1570 @Override
1571 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1572 String[] recips = tokenizeRecips(getAddressesFromList(mView));
1573 for (String recip : recips) {
1574 if (!mContent.containsKey(recip)) {
1575 mContent.put(recip, 1);
1576 } else {
1577 mContent.put(recip, (mContent.get(recip)) + 1);
1578 }
1579 }
1580 }
1581
1582 @Override
1583 public void onTextChanged(CharSequence s, int start, int before, int count) {
1584 // Do nothing.
1585 }
1586 }
Mindy Pereira181df782012-03-01 13:32:44 -08001587
1588 @Override
1589 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1590 if (id == ACCOUNT_SETTINGS_LOADER) {
1591 if (mAccount.settingsQueryUri != null) {
1592 return new CursorLoader(this, mAccount.settingsQueryUri,
1593 UIProvider.SETTINGS_PROJECTION, null, null, null);
1594 }
1595 }
1596 return null;
1597 }
1598
1599 @Override
1600 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1601 if (loader.getId() == ACCOUNT_SETTINGS_LOADER) {
1602 if (data != null) {
1603 data.moveToFirst();
1604 mCachedSettings = new Settings(data);
1605 }
1606 }
1607 }
1608
1609 @Override
1610 public void onLoaderReset(Loader<Cursor> loader) {
1611 // Do nothing.
1612 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001613}