blob: 77fc957af400966f55da3beb49f09e8d7f5c0aa8 [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;
Paul Westbrookf97588b2012-03-20 11:11:37 -070039import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080040import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080041import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080042import android.text.Html;
43import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080044import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080045import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080046import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080047import android.text.util.Rfc822Tokenizer;
Mindy Pereira326c6602012-01-04 15:32:42 -080048import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080049import android.view.Menu;
50import android.view.MenuInflater;
51import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080052import android.view.View;
Mindy Pereira326c6602012-01-04 15:32:42 -080053import android.view.ViewGroup;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080054import android.view.View.OnClickListener;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080055import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080056import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080057import android.widget.Button;
Mindy Pereira1f936682012-03-02 11:30:33 -080058import android.widget.ImageView;
Mindy Pereira6349a042012-01-04 11:25:01 -080059import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080060import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080061
Mindy Pereirac17d0732011-12-29 10:46:19 -080062import com.android.common.Rfc822Validator;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080063import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080064import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080065import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080066import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080067import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080068import com.android.mail.providers.Address;
69import com.android.mail.providers.Attachment;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080070import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080071import com.android.mail.providers.MessageModification;
Mindy Pereira181df782012-03-01 13:32:44 -080072import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080073import com.android.mail.providers.UIProvider;
Mindy Pereira82cc5662012-01-09 17:29:30 -080074import com.android.mail.providers.UIProvider.MessageColumns;
Andy Huang30e2c242012-01-06 18:14:30 -080075import com.android.mail.R;
Paul Westbrook92227f62012-03-20 10:32:51 -070076import com.android.mail.utils.AccountUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080077import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080078import com.android.mail.utils.Utils;
Mindy Pereirac17d0732011-12-29 10:46:19 -080079import com.android.ex.chips.RecipientEditTextView;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080080import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080081import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080082import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080083
Mindy Pereira46ce0b12012-01-05 10:32:15 -080084import java.util.ArrayList;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080085import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080086import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080087import java.util.HashSet;
88import java.util.List;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080089import java.util.Set;
Mindy Pereira75f66632012-01-11 11:42:02 -080090import java.util.Map.Entry;
Mindy Pereira82cc5662012-01-09 17:29:30 -080091import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080092
93public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080094 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Mindy Pereira181df782012-03-01 13:32:44 -080095 AttachmentDeletedListener, OnAccountChangedListener, LoaderCallbacks<Cursor> {
Mindy Pereira6349a042012-01-04 11:25:01 -080096 // Identifiers for which type of composition this is
97 static final int COMPOSE = -1; // also used for editing a draft
98 static final int REPLY = 0;
99 static final int REPLY_ALL = 1;
100 static final int FORWARD = 2;
101
102 // Integer extra holding one of the above compose action
103 private static final String EXTRA_ACTION = "action";
104
Mindy Pereira82cc5662012-01-09 17:29:30 -0800105 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
106 // Map containing information about requests to create new messages, and the id of the
107 // messages that were the result of those requests.
108 //
109 // This map is used when the activity that initiated the save a of a new message, is killed
110 // before the save has completed (and when we know the id of the newly created message). When
111 // a save is completed, the service that is running in the background, will update the map
112 //
113 // When a new ComposeActivity instance is created, it will attempt to use the information in
114 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
115 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
116 // to populate the map with data stored in shared preferences.
117 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
118 // Key used to store the above map
119 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800120 /**
121 * Notifies the {@code Activity} that the caller is an Email
122 * {@code Activity}, so that the back behavior may be modified accordingly.
123 *
124 * @see #onAppUpPressed
125 */
126 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
127
Paul Westbrookf97588b2012-03-20 11:11:37 -0700128 static final String EXTRA_ATTACHMENTS = "attachments";
129
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800130 // If this is a reply/forward then this extra will hold the original message
131 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800132 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800133 private static final String LOG_TAG = new LogUtils().getLogTag();
134 // Request numbers for activities we start
135 private static final int RESULT_PICK_ATTACHMENT = 1;
136 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira181df782012-03-01 13:32:44 -0800137 private static final int ACCOUNT_SETTINGS_LOADER = 0;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800138
Mindy Pereira82cc5662012-01-09 17:29:30 -0800139 /**
140 * A single thread for running tasks in the background.
141 */
142 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800143 private RecipientEditTextView mTo;
144 private RecipientEditTextView mCc;
145 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800146 private Button mCcBccButton;
147 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800148 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800149 private Account mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800150 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800151 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800152 private TextView mSubject;
153
Mindy Pereira326c6602012-01-04 15:32:42 -0800154 private ComposeModeAdapter mComposeModeAdapter;
155 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800156 private boolean mForward;
157 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800158 private QuotedTextView mQuotedTextView;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800159 private TextView mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800160 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800161 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800162 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800163 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800164 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800165 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800166 private boolean mTextChanged;
167 private boolean mReplyFromChanged;
168 private MenuItem mSave;
169 private MenuItem mSend;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800170 private String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800171 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800172 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800173 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800174 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
175 private Message mDraft;
176 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800177 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800178
Mindy Pereira326c6602012-01-04 15:32:42 -0800179 /**
180 * Can be called from a non-UI thread.
181 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800182 public static void editDraft(Context launcher, Account account, Message message) {
Mindy Pereira326c6602012-01-04 15:32:42 -0800183 }
184
Mindy Pereira6349a042012-01-04 11:25:01 -0800185 /**
186 * Can be called from a non-UI thread.
187 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800188 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800189 launch(launcher, account, null, COMPOSE);
190 }
191
192 /**
193 * Can be called from a non-UI thread.
194 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800195 public static void reply(Context launcher, Account account, Message message) {
196 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800197 }
198
199 /**
200 * Can be called from a non-UI thread.
201 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800202 public static void replyAll(Context launcher, Account account, Message message) {
203 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800204 }
205
206 /**
207 * Can be called from a non-UI thread.
208 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800209 public static void forward(Context launcher, Account account, Message message) {
210 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800211 }
212
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800213 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800214 Intent intent = new Intent(launcher, ComposeActivity.class);
215 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
216 intent.putExtra(EXTRA_ACTION, action);
217 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800218 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
Mindy Pereira6349a042012-01-04 11:25:01 -0800219 launcher.startActivity(intent);
220 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800221
222 @Override
223 public void onCreate(Bundle savedInstanceState) {
224 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800225 setContentView(R.layout.compose);
226 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800227 Intent intent = getIntent();
Paul Westbrook92227f62012-03-20 10:32:51 -0700228
229 Account account = (Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
230 if (account == null) {
231 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
232 if (syncingAccounts.length > 0) {
233 account = syncingAccounts[0];
234 }
235 }
236
237 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800238 if (mAccount == null) {
239 return;
240 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800241 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800242 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereira29ef1b82012-01-13 11:26:21 -0800243 if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800244 initFromRefMessage(action, mAccount.name);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800245 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800246 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800247 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800248 initRecipients();
Paul Westbrookf97588b2012-03-20 11:11:37 -0700249 initAttachmentsFromIntent(intent);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800250 initActionBar(action);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800251 initFromSpinner();
Mindy Pereira75f66632012-01-11 11:42:02 -0800252 initChangeListeners();
Mindy Pereira1a95a572012-01-05 12:21:29 -0800253 }
254
255 @Override
256 protected void onResume() {
257 super.onResume();
258 // Update the from spinner as other accounts
259 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800260 if (mFromSpinner != null && mAccount != null) {
261 mFromSpinner.asyncInitFromSpinner();
262 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800263 }
264
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800265 @Override
266 protected void onPause() {
267 super.onPause();
268
269 if (mSendConfirmDialog != null) {
270 mSendConfirmDialog.dismiss();
271 }
272 if (mRecipientErrorDialog != null) {
273 mRecipientErrorDialog.dismiss();
274 }
275
276 saveIfNeeded();
277 }
278
279 @Override
280 protected final void onActivityResult(int request, int result, Intent data) {
281 mAddingAttachment = false;
282
283 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
284 addAttachmentAndUpdateView(data);
285 }
286 }
287
288 @Override
289 public final void onSaveInstanceState(Bundle state) {
290 super.onSaveInstanceState(state);
291
292 // onSaveInstanceState is only called if the user might come back to this activity so it is
293 // not an ideal location to save the draft. However, if we have never saved the draft before
294 // we have to save it here in order to have an id to save in the bundle.
295 saveIfNeededOnOrientationChanged();
296 }
297
Mindy Pereira818143e2012-01-11 13:59:49 -0800298 @VisibleForTesting
299 void setAccount(Account account) {
300 mAccount = account;
Mindy Pereira181df782012-03-01 13:32:44 -0800301 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800302 }
303
Mindy Pereira1a95a572012-01-05 12:21:29 -0800304 private void initFromSpinner() {
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800305 mFromSpinner.setCurrentAccount(mAccount);
306 mFromSpinner.asyncInitFromSpinner();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800307 boolean showSpinner = mFromSpinner.getCount() > 1;
308 // If there is only 1 account, just show that account.
309 // Otherwise, give the user the ability to choose which account to send
310 // mail from / save drafts to.
311 mFromStatic.setVisibility(
312 showSpinner ? View.GONE : View.VISIBLE);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800313 mFromStaticText.setText(mAccount.name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800314 mFromSpinnerWrapper.setVisibility(
315 showSpinner ? View.VISIBLE : View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800316 }
317
318 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800319 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800320 if (mCcBccButton != null) {
321 mCcBccButton.setOnClickListener(this);
322 }
323 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800324 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800325 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
326 if (mAttachmentsButton != null) {
327 mAttachmentsButton.setOnClickListener(this);
328 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800329 mTo = (RecipientEditTextView) findViewById(R.id.to);
330 mCc = (RecipientEditTextView) findViewById(R.id.cc);
331 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800332 // TODO: add special chips text change watchers before adding
333 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800334 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800335 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
336 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800337 mBodyView = (TextView) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800338 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800339 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800340 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800341 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800342 }
343
Mindy Pereira75f66632012-01-11 11:42:02 -0800344 // Now that the message has been initialized from any existing draft or
345 // ref message data, set up listeners for any changes that occur to the
346 // message.
347 private void initChangeListeners() {
348 mSubject.addTextChangedListener(this);
349 mBodyView.addTextChangedListener(this);
350 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
351 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
352 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
353 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800354 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800355 }
356
Mindy Pereira326c6602012-01-04 15:32:42 -0800357 private void initActionBar(int action) {
358 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800359 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800360 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800361 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
362 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800363 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800364 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800365 if (mComposeModeAdapter == null) {
366 mComposeModeAdapter = new ComposeModeAdapter(this);
367 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800368 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
369 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800370 switch (action) {
371 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800372 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800373 break;
374 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800375 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800376 break;
377 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800378 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800379 break;
380 }
381 }
382 }
383
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800384 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800385 if (mRefMessage != null) {
386 mRefMessageId = mRefMessage.refMessageId;
387 setSubject(mRefMessage, action);
388 // Setup recipients
389 if (action == FORWARD) {
390 mForward = true;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800391 }
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800392 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
393 initBodyFromRefMessage(mRefMessage, action);
394 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
395 initAttachments(mRefMessage);
396 }
397 updateHideOrShowCcBcc();
Mindy Pereira6349a042012-01-04 11:25:01 -0800398 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800399 }
400
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800401 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800402 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800403 }
404
Paul Westbrookf97588b2012-03-20 11:11:37 -0700405 private void initAttachmentsFromIntent(Intent intent) {
406 final Bundle extras = intent.getExtras();
407 final String action = intent.getAction();
408 if (!mAttachmentsChanged) {
409 long totalSize = 0;
410 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
411 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
412 for (String uriString : uris) {
413 final Uri uri = Uri.parse(uriString);
414 long size = 0;
415 try {
416 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
417 true /* local file */);
418 } catch (AttachmentFailureException e) {
419 // A toast has already been shown to the user,
420 // just break out of the loop.
421 LogUtils.e(LOG_TAG, e, "Error adding attachment");
422 }
423 totalSize += size;
424 }
425 }
426 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
427 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
428 long size = 0;
429 try {
430 size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
431 true /* local file */);
432 } catch (AttachmentFailureException e) {
433 // A toast has already been shown to the user, so just
434 // exit.
435 LogUtils.e(LOG_TAG, e, "Error adding attachment");
436 }
437 totalSize += size;
438 }
439
440 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
441 && extras.containsKey(Intent.EXTRA_STREAM)) {
442 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
443 for (Parcelable uri : uris) {
444 long size = 0;
445 try {
446 size = mAttachmentsView.addAttachment(mAccount, (Uri)uri,
447 false /* doSave */, true /* local file */);
448 } catch (AttachmentFailureException e) {
449 // A toast has already been shown to the user,
450 // just break out of the loop.
451 LogUtils.e(LOG_TAG, e, "Error adding attachment");
452 }
453 totalSize += size;
454 }
455 }
456
457 if (totalSize > 0) {
458 mAttachmentsChanged = true;
459 updateSaveUi();
460 }
461 }
462 }
463
464
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800465 private void initBodyFromRefMessage(Message refMessage, int action) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800466 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
Mindy Pereira9932dee2012-01-10 16:09:50 -0800467 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
468 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800469 }
470
471 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800472 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800473 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
474 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
475 if (ccVisible || bccVisible) {
476 mCcBccView.show(false, ccVisible, bccVisible);
477 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800478 if (mCcBccButton != null) {
479 if (!mCc.isShown() || !mBcc.isShown()) {
480 mCcBccButton.setVisibility(View.VISIBLE);
481 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
482 : R.string.add_bcc_label));
483 } else {
484 mCcBccButton.setVisibility(View.GONE);
485 }
486 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800487 }
488
Mindy Pereira013194c2012-01-06 15:09:33 -0800489 /**
490 * Add attachment and update the compose area appropriately.
491 * @param data
492 */
493 public void addAttachmentAndUpdateView(Intent data) {
494 Uri uri = data != null ? data.getData() : null;
Mindy Pereira013194c2012-01-06 15:09:33 -0800495 try {
Mindy Pereiraf944e962012-01-17 11:43:36 -0800496 long size = mAttachmentsView.addAttachment(mAccount, uri, false /* doSave */,
497 true /* local file */);
Mindy Pereira9932dee2012-01-10 16:09:50 -0800498 if (size > 0) {
499 mAttachmentsChanged = true;
500 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -0800501 }
Mindy Pereira9932dee2012-01-10 16:09:50 -0800502 } catch (AttachmentFailureException e) {
503 // A toast has already been shown to the user, no need to do
504 // anything.
505 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -0800506 }
507 }
508
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800509 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800510 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800511 // Don't populate the address if this is a forward.
512 if (action == ComposeActivity.FORWARD) {
513 return;
514 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800515 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800516 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800517
Mindy Pereira818143e2012-01-11 13:59:49 -0800518 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800519 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800520 // This is the email address of the current user, i.e. the one composing
521 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800522 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800523 String fromAddress = refMessage.from;
524 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
525 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800526 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800527
528 // If this is a reply, the Cc list is empty. If this is a reply-all, the
529 // Cc list is the union of the To and Cc recipients of the original
530 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800531 // already on the To list.
532 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800533 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
534 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800535 addToAddresses(toAddresses);
536 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800537 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800538 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
539 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -0800540 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800541 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800542 addRecipients(accountEmail, ccAddresses,
543 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800544 addCcAddresses(ccAddresses, toAddresses);
545 }
546 }
547
548 private void addToAddresses(Collection<String> addresses) {
549 addAddressesToList(addresses, mTo);
550 }
551
552 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
553 addCcAddressesToList(tokenizeAddressList(addresses), tokenizeAddressList(toAddresses),
554 mCc);
555 }
556
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800557 @VisibleForTesting
558 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
559 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
560 String address;
561
562 HashSet<String> compareTo = convertToHashSet(compareToList);
563 for (Rfc822Token[] tokens : addresses) {
564 for (int i = 0; i < tokens.length; i++) {
565 address = tokens[i].toString();
566 // Check if this is a duplicate:
567 if (!compareTo.contains(tokens[i].getAddress())) {
568 // Get the address here
569 list.append(address + END_TOKEN);
570 }
571 }
572 }
573 }
574
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800575 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
576 HashSet<String> hash = new HashSet<String>();
577 for (Rfc822Token[] tokens : list) {
578 for (int i = 0; i < tokens.length; i++) {
579 hash.add(tokens[i].getAddress());
580 }
581 }
582 return hash;
583 }
584
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800585 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
586 @VisibleForTesting
587 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
588
589 for (String address: addresses) {
590 tokenized.add(Rfc822Tokenizer.tokenize(address));
591 }
592 return tokenized;
593 }
594
595 @VisibleForTesting
596 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
597 for (String address : addresses) {
598 addAddressToList(address, list);
599 }
600 }
601
602 private void addAddressToList(String address, RecipientEditTextView list) {
603 if (address == null || list == null)
604 return;
605
606 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
607
608 for (int i = 0; i < tokens.length; i++) {
609 list.append(tokens[i] + END_TOKEN);
610 }
611 }
612
613 @VisibleForTesting
614 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800615 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800616 // The To recipient is the reply-to address specified in the original
617 // message, unless it is:
618 // the current user OR a custom from of the current user, in which case
619 // it's the To recipient list of the original message.
620 // OR missing, in which case use the sender of the original message
621 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800622 if (!TextUtils.isEmpty(replyToAddress)) {
623 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800624 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800625 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800626 }
627 return toAddresses;
628 }
629
630 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
631 for (String email : addresses) {
632 // Do not add this account, or any of the custom froms, to the list
633 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800634 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800635 if (!account.equalsIgnoreCase(recipientAddress)) {
636 recipients.add(email.replace("\"\"", ""));
637 }
638 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800639 }
640
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800641 private void setSubject(Message refMessage, int action) {
642 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800643 String prefix;
644 String correctedSubject = null;
645 if (action == ComposeActivity.COMPOSE) {
646 prefix = "";
647 } else if (action == ComposeActivity.FORWARD) {
648 prefix = getString(R.string.forward_subject_label);
649 } else {
650 prefix = getString(R.string.reply_subject_label);
651 }
652
653 // Don't duplicate the prefix
654 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
655 correctedSubject = subject;
656 } else {
657 correctedSubject = String
658 .format(getString(R.string.formatted_subject), prefix, subject);
659 }
660 mSubject.setText(correctedSubject);
661 }
662
Mindy Pereira818143e2012-01-11 13:59:49 -0800663 private void initRecipients() {
664 setupRecipients(mTo);
665 setupRecipients(mCc);
666 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800667 }
668
Mindy Pereira818143e2012-01-11 13:59:49 -0800669 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800670 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -0800671 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -0800672 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -0800673 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800674 int offset = accountName.indexOf("@") + 1;
675 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800676 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -0800677 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800678 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800679 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800680 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800681 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800682 }
683
684 @Override
685 public void onClick(View v) {
686 int id = v.getId();
687 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800688 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800689 // Verify that cc/ bcc aren't showing.
690 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800691 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800692 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800693 case R.id.add_attachment:
694 doAttach();
695 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800696 }
697 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800698
699 @Override
700 public boolean onCreateOptionsMenu(Menu menu) {
701 super.onCreateOptionsMenu(menu);
702 MenuInflater inflater = getMenuInflater();
703 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800704 mSave = menu.findItem(R.id.save);
705 mSend = menu.findItem(R.id.send);
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800706 return true;
707 }
708
709 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800710 public boolean onPrepareOptionsMenu(Menu menu) {
711 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -0800712 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800713 // Its possible there is a menu item OR a button.
714 boolean ccFieldVisible = mCc.isShown();
715 boolean bccFieldVisible = mBcc.isShown();
716 if (!ccFieldVisible || !bccFieldVisible) {
717 ccBcc.setVisible(true);
718 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
719 : R.string.add_bcc_label));
720 } else {
721 ccBcc.setVisible(false);
722 }
723 }
Mindy Pereira75f66632012-01-11 11:42:02 -0800724 if (mSave != null) {
725 mSave.setEnabled(shouldSave());
726 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800727 return true;
728 }
729
730 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800731 public boolean onOptionsItemSelected(MenuItem item) {
732 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -0800733 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800734 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -0800735 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -0800736 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -0800737 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800738 case R.id.add_cc_bcc:
739 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800740 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800741 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800742 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800743 break;
744 case R.id.send:
745 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -0800746 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -0800747 case R.id.discard:
748 doDiscard();
749 break;
Mindy Pereira1f936682012-03-02 11:30:33 -0800750 case R.id.settings:
751 Utils.showSettings(this, mAccount);
752 break;
Mindy Pereira75f66632012-01-11 11:42:02 -0800753 default:
754 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800755 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800756 }
757 return !handled ? super.onOptionsItemSelected(item) : handled;
758 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800759
Mindy Pereira33fe9082012-01-09 16:24:30 -0800760 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800761 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -0800762 }
763
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800764 private void doSave(boolean showToast, boolean resetIME) {
765 sendOrSaveWithSanityChecks(true, showToast, false);
766 if (resetIME) {
767 // Clear the IME composing suggestions from the body.
768 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
769 }
Mindy Pereira33fe9082012-01-09 16:24:30 -0800770 }
771
Mindy Pereira82cc5662012-01-09 17:29:30 -0800772 /*package*/ interface SendOrSaveCallback {
773 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800774 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
775 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -0800776 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
777 }
778
779 /*package*/ static class SendOrSaveTask implements Runnable {
780 private final Context mContext;
781 private final SendOrSaveCallback mSendOrSaveCallback;
782 @VisibleForTesting
783 final SendOrSaveMessage mSendOrSaveMessage;
784
785 public SendOrSaveTask(Context context, SendOrSaveMessage message,
786 SendOrSaveCallback callback) {
787 mContext = context;
788 mSendOrSaveCallback = callback;
789 mSendOrSaveMessage = message;
790 }
791
792 @Override
793 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800794 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800795
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800796 final Account selectedAccount = sendOrSaveMessage.mSelectedAccount;
797 Message message = mSendOrSaveCallback.getMessage();
798 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800799 // If a previous draft has been saved, in an account that is different
800 // than what the user wants to send from, remove the old draft, and treat this
801 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800802 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800803 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
804 ContentResolver resolver = mContext.getContentResolver();
805 ContentValues values = new ContentValues();
806 values.put(BaseColumns._ID, messageId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -0800807 if (selectedAccount.expungeMessageUri != null) {
808 resolver.update(selectedAccount.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800809 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -0800810 } else {
811 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800812 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800813 // reset messageId to 0, so a new message will be created
814 messageId = UIProvider.INVALID_MESSAGE_ID;
815 }
816 }
817
818 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800819 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800820 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800821 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800822 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
823 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800824 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800825 ContentResolver resolver = mContext.getContentResolver();
826 Uri messageUri = resolver.insert(
Mindy Pereiracfb7f332012-02-28 10:23:43 -0800827 sendOrSaveMessage.mSave ? selectedAccount.saveDraftUri
828 : selectedAccount.sendMessageUri, sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800829 if (sendOrSaveMessage.mSave && messageUri != null) {
830 Cursor messageCursor = resolver.query(messageUri,
831 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -0700832 if (messageCursor != null) {
833 try {
834 if (messageCursor.moveToFirst()) {
835 // Broadcast notification that a new message has
836 // been allocated
837 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
838 new Message(messageCursor));
839 }
840 } finally {
841 messageCursor.close();
842 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800843 }
844 }
Mindy Pereira82cc5662012-01-09 17:29:30 -0800845 }
846
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800847 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800848 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800849 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -0800850 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800851 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -0800852 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800853 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -0800854 }
855 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
856 }
857 }
858
859 // Array of the outstanding send or save tasks. Access is synchronized
860 // with the object itself
861 /* package for testing */
862 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
863 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -0800864 private String mSignature;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800865
866 /*package*/ static class SendOrSaveMessage {
867 final Account mAccount;
868 final Account mSelectedAccount;
869 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800870 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800871 final boolean mSave;
872 final int mRequestId;
873
874 public SendOrSaveMessage(Account account, Account selectedAccount, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800875 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800876 mAccount = account;
877 mSelectedAccount = selectedAccount;
878 mValues = values;
879 mRefMessageId = refMessageId;
880 mSave = save;
881 mRequestId = mValues.hashCode() ^ hashCode();
882 }
883
884 int requestId() {
885 return mRequestId;
886 }
887 }
888
889 /**
890 * Get the to recipients.
891 */
892 public String[] getToAddresses() {
893 return getAddressesFromList(mTo);
894 }
895
896 /**
897 * Get the cc recipients.
898 */
899 public String[] getCcAddresses() {
900 return getAddressesFromList(mCc);
901 }
902
903 /**
904 * Get the bcc recipients.
905 */
906 public String[] getBccAddresses() {
907 return getAddressesFromList(mBcc);
908 }
909
910 public String[] getAddressesFromList(RecipientEditTextView list) {
911 if (list == null) {
912 return new String[0];
913 }
914 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
915 int count = tokens.length;
916 String[] result = new String[count];
917 for (int i = 0; i < count; i++) {
918 result[i] = tokens[i].toString();
919 }
920 return result;
921 }
922
923 /**
924 * Check for invalid email addresses.
925 * @param to String array of email addresses to check.
926 * @param wrongEmailsOut Emails addresses that were invalid.
927 */
928 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
929 for (String email : to) {
930 if (!mValidator.isValid(email)) {
931 wrongEmailsOut.add(email);
932 }
933 }
934 }
935
936 /**
937 * Show an error because the user has entered an invalid recipient.
938 * @param message
939 */
940 public void showRecipientErrorDialog(String message) {
941 // Only 1 invalid recipients error dialog should be allowed up at a
942 // time.
943 if (mRecipientErrorDialog != null) {
944 mRecipientErrorDialog.dismiss();
945 }
946 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
947 R.string.recipient_error_dialog_title)
948 .setIconAttribute(android.R.attr.alertDialogIcon)
949 .setCancelable(false)
950 .setPositiveButton(
951 R.string.ok, new Dialog.OnClickListener() {
952 public void onClick(DialogInterface dialog, int which) {
953 // after the user dismisses the recipient error
954 // dialog we want to make sure to refocus the
955 // recipient to field so they can fix the issue
956 // easily
957 if (mTo != null) {
958 mTo.requestFocus();
959 }
960 mRecipientErrorDialog = null;
961 }
962 }).show();
963 }
964
965 /**
966 * Update the state of the UI based on whether or not the current draft
967 * needs to be saved and the message is not empty.
968 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800969 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800970 if (mSave != null) {
971 mSave.setEnabled((shouldSave() && !isBlank()));
972 }
973 }
974
975 /**
976 * Returns true if we need to save the current draft.
977 */
978 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800979 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -0800980 // The message should only be saved if:
981 // It hasn't been sent AND
982 // Some text has been added to the message OR
983 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800984 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -0800985 (mReplyFromChanged && !isBlank()));
986 }
987 }
988
989 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -0800990 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800991 * @return boolean
992 */
993 public boolean isBlank() {
994 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -0800995 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
996 mBodyView.getText().toString()) == 0)
997 && mTo.length() == 0
998 && mCc.length() == 0 && mBcc.length() == 0
999 && mAttachmentsView.getAttachments().size() == 0;
1000 }
1001
1002 @VisibleForTesting
1003 protected int getSignatureStartPosition(String signature, String bodyText) {
1004 int startPos = -1;
1005
1006 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1007 return startPos;
1008 }
1009
1010 int bodyLength = bodyText.length();
1011 int signatureLength = signature.length();
1012 String printableVersion = convertToPrintableSignature(signature);
1013 int printableLength = printableVersion.length();
1014
1015 if (bodyLength >= printableLength
1016 && bodyText.substring(bodyLength - printableLength)
1017 .equals(printableVersion)) {
1018 startPos = bodyLength - printableLength;
1019 } else if (bodyLength >= signatureLength
1020 && bodyText.substring(bodyLength - signatureLength)
1021 .equals(signature)) {
1022 startPos = bodyLength - signatureLength;
1023 }
1024 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001025 }
1026
1027 /**
1028 * Allows any changes made by the user to be ignored. Called when the user
1029 * decides to discard a draft.
1030 */
1031 private void discardChanges() {
1032 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001033 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001034 mReplyFromChanged = false;
1035 }
1036
1037 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001038 * @param body
1039 * @param save
1040 * @param showToast
1041 * @return Whether the send or save succeeded.
1042 */
1043 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1044 final boolean orientationChanged) {
1045 String[] to, cc, bcc;
1046 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001047
Mindy Pereira181df782012-03-01 13:32:44 -08001048 if (orientationChanged) {
1049 to = cc = bcc = new String[0];
1050 } else {
1051 to = getToAddresses();
1052 cc = getCcAddresses();
1053 bcc = getBccAddresses();
1054 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001055
Mindy Pereira181df782012-03-01 13:32:44 -08001056 // Don't let the user send to nobody (but it's okay to save a message
1057 // with no recipients)
1058 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1059 showRecipientErrorDialog(getString(R.string.recipient_needed));
1060 return false;
1061 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001062
Mindy Pereira181df782012-03-01 13:32:44 -08001063 List<String> wrongEmails = new ArrayList<String>();
1064 if (!save) {
1065 checkInvalidEmails(to, wrongEmails);
1066 checkInvalidEmails(cc, wrongEmails);
1067 checkInvalidEmails(bcc, wrongEmails);
1068 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001069
Mindy Pereira181df782012-03-01 13:32:44 -08001070 // Don't let the user send an email with invalid recipients
1071 if (wrongEmails.size() > 0) {
1072 String errorText = String.format(getString(R.string.invalid_recipient),
1073 wrongEmails.get(0));
1074 showRecipientErrorDialog(errorText);
1075 return false;
1076 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001077
Mindy Pereira181df782012-03-01 13:32:44 -08001078 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
1079 public void onClick(DialogInterface dialog, int which) {
1080 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1081 }
1082 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001083
Mindy Pereira181df782012-03-01 13:32:44 -08001084 // Show a warning before sending only if there are no attachments.
1085 if (!save) {
1086 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1087 boolean warnAboutEmptySubject = isSubjectEmpty();
1088 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001089
Mindy Pereira181df782012-03-01 13:32:44 -08001090 // A warning about an empty body may not be warranted when
1091 // forwarding mails, since a common use case is to forward
1092 // quoted text and not append any more text.
1093 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001094
Mindy Pereira181df782012-03-01 13:32:44 -08001095 // When we bring up a dialog warning the user about a send,
1096 // assume that they accept sending the message. If they do not,
1097 // the dialog listener is required to enable sending again.
1098 if (warnAboutEmptySubject) {
1099 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1100 return true;
1101 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001102
Mindy Pereira181df782012-03-01 13:32:44 -08001103 if (warnAboutEmptyBody) {
1104 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1105 return true;
1106 }
1107 }
1108 // Ask for confirmation to send (if always required)
1109 if (showSendConfirmation()) {
1110 showSendConfirmDialog(R.string.confirm_send_message, listener);
1111 return true;
1112 }
1113 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001114
Mindy Pereira181df782012-03-01 13:32:44 -08001115 sendOrSave(body, save, showToast, false);
1116 return true;
1117 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001118
Mindy Pereira181df782012-03-01 13:32:44 -08001119 /**
1120 * Returns a boolean indicating whether warnings should be shown for empty
1121 * subject and body fields
1122 *
1123 * @return True if a warning should be shown for empty text fields
1124 */
1125 protected boolean showEmptyTextWarnings() {
1126 return mAttachmentsView.getAttachments().size() == 0;
1127 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001128
Mindy Pereira181df782012-03-01 13:32:44 -08001129 /**
1130 * Returns a boolean indicating whether the user should confirm each send
1131 *
1132 * @return True if a warning should be on each send
1133 */
1134 protected boolean showSendConfirmation() {
1135 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1136 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001137
Mindy Pereira181df782012-03-01 13:32:44 -08001138 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1139 if (mSendConfirmDialog != null) {
1140 mSendConfirmDialog.dismiss();
1141 mSendConfirmDialog = null;
1142 }
1143 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1144 .setTitle(R.string.confirm_send_title)
1145 .setIconAttribute(android.R.attr.alertDialogIcon)
1146 .setPositiveButton(R.string.send, listener)
1147 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1148 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001149
Mindy Pereira181df782012-03-01 13:32:44 -08001150 /**
1151 * Returns whether the ComposeArea believes there is any text in the body of
1152 * the composition. TODO: When ComposeArea controls the Body as well, add
1153 * that here.
1154 */
1155 public boolean isBodyEmpty() {
1156 return !mQuotedTextView.isTextIncluded();
1157 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001158
Mindy Pereira181df782012-03-01 13:32:44 -08001159 /**
1160 * Test to see if the subject is empty.
1161 *
1162 * @return boolean.
1163 */
1164 // TODO: this will likely go away when composeArea.focus() is implemented
1165 // after all the widget control is moved over.
1166 public boolean isSubjectEmpty() {
1167 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1168 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001169
Mindy Pereira181df782012-03-01 13:32:44 -08001170 /* package */
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001171 static int sendOrSaveInternal(Context context, final Account account,
1172 final Account selectedAccount, String fromAddress, final Spanned body,
1173 final String[] to, final String[] cc, final String[] bcc, final String subject,
1174 final CharSequence quotedText, final List<Attachment> attachments,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001175 final String refMessageId, SendOrSaveCallback callback, Handler handler, boolean save,
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001176 boolean forward) {
1177 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001178
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001179 MessageModification.putToAddresses(values, to);
1180 MessageModification.putCcAddresses(values, cc);
1181 MessageModification.putBccAddresses(values, bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001182
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001183 MessageModification.putSubject(values, subject);
1184 String htmlBody = Html.toHtml(body);
1185 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1186 StringBuilder fullBody = new StringBuilder(htmlBody);
1187 if (includeQuotedText) {
1188 if (forward) {
1189 // forwarded messages get full text in HTML from client
1190 fullBody.append(quotedText);
1191 MessageModification.putForward(values, forward);
1192 } else {
1193 // replies get full quoted text from server - HTMl gets
1194 // converted to text for now
1195 final String text = quotedText.toString();
1196 if (QuotedTextView.containsQuotedText(text)) {
1197 int pos = QuotedTextView.getQuotedTextOffset(text);
1198 fullBody.append(text.substring(0, pos));
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001199 MessageModification.putForward(values, forward);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001200 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001201 } else {
1202 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1203 // This shouldn't happen, but just use what we have,
1204 // and don't do server-side expansion
1205 fullBody.append(text);
1206 }
1207 }
1208 }
1209 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1210 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereiraf944e962012-01-17 11:43:36 -08001211 MessageModification.putAttachments(values, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001212
Mindy Pereira181df782012-03-01 13:32:44 -08001213 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(account, selectedAccount,
1214 values, refMessageId, save);
1215 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001216
Mindy Pereira181df782012-03-01 13:32:44 -08001217 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001218
Mindy Pereira181df782012-03-01 13:32:44 -08001219 // Do the send/save action on the specified handler to avoid possible
1220 // ANRs
1221 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001222
Mindy Pereira181df782012-03-01 13:32:44 -08001223 return sendOrSaveMessage.requestId();
1224 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001225
Mindy Pereira181df782012-03-01 13:32:44 -08001226 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1227 boolean orientationChanged) {
1228 // Check if user is a monkey. Monkeys can compose and hit send
1229 // button but are not allowed to send anything off the device.
1230 if (!save && ActivityManager.isUserAMonkey()) {
1231 return;
1232 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001233
Mindy Pereira181df782012-03-01 13:32:44 -08001234 String[] to, cc, bcc;
1235 if (orientationChanged) {
1236 to = cc = bcc = new String[0];
1237 } else {
1238 to = getToAddresses();
1239 cc = getCcAddresses();
1240 bcc = getBccAddresses();
1241 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001242
Mindy Pereira181df782012-03-01 13:32:44 -08001243 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001244 private int mRestoredRequestId;
1245
1246 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08001247 synchronized (mActiveTasks) {
1248 int numTasks = mActiveTasks.size();
1249 if (numTasks == 0) {
1250 // Start service so we won't be killed if this app is
1251 // put in the background.
1252 startService(new Intent(ComposeActivity.this, EmptyService.class));
1253 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001254
Mindy Pereira181df782012-03-01 13:32:44 -08001255 mActiveTasks.add(sendOrSaveTask);
1256 }
1257 if (sTestSendOrSaveCallback != null) {
1258 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1259 }
1260 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001261
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001262 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1263 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08001264 synchronized (mDraftLock) {
1265 mDraftId = message.id;
1266 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001267 if (sRequestMessageIdMap != null) {
1268 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1269 }
Mindy Pereira181df782012-03-01 13:32:44 -08001270 // Cache request message map, in case the process is killed
1271 saveRequestMap();
1272 }
1273 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001274 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08001275 }
1276 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001277
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001278 public Message getMessage() {
1279 synchronized (mDraftLock) {
1280 return mDraft;
1281 }
1282 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001283
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001284 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1285 if (success) {
1286 // Successfully sent or saved so reset change markers
1287 discardChanges();
1288 } else {
1289 // A failure happened with saving/sending the draft
1290 // TODO(pwestbro): add a better string that should be used
1291 // when failing to send or save
1292 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1293 .show();
1294 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001295
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001296 int numTasks;
1297 synchronized (mActiveTasks) {
1298 // Remove the task from the list of active tasks
1299 mActiveTasks.remove(task);
1300 numTasks = mActiveTasks.size();
1301 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001302
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001303 if (numTasks == 0) {
1304 // Stop service so we can be killed.
1305 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1306 }
1307 if (sTestSendOrSaveCallback != null) {
1308 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1309 }
1310 }
Mindy Pereira181df782012-03-01 13:32:44 -08001311 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001312
Mindy Pereira181df782012-03-01 13:32:44 -08001313 // Get the selected account if the from spinner has been setup.
1314 Account selectedAccount = mAccount;
1315 String fromAddress = selectedAccount.name;
1316 if (selectedAccount == null || fromAddress == null) {
1317 // We don't have either the selected account or from address,
1318 // use mAccount.
1319 selectedAccount = mAccount;
1320 fromAddress = mAccount.name;
1321 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001322
Mindy Pereira181df782012-03-01 13:32:44 -08001323 if (mSendSaveTaskHandler == null) {
1324 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1325 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001326
Mindy Pereira181df782012-03-01 13:32:44 -08001327 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1328 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001329
Mindy Pereira181df782012-03-01 13:32:44 -08001330 mRequestId = sendOrSaveInternal(this, mAccount, selectedAccount, fromAddress, body, to, cc,
1331 bcc, mSubject.getText().toString(), mQuotedTextView.getQuotedText(),
1332 mAttachmentsView.getAttachments(), mRefMessageId, callback, mSendSaveTaskHandler,
1333 save, mForward);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001334
Mindy Pereira181df782012-03-01 13:32:44 -08001335 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1336 mRecipient = selectedAccount.name;
1337 }
1338 mAccount = selectedAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001339
Mindy Pereira181df782012-03-01 13:32:44 -08001340 // Don't display the toast if the user is just changing the orientation,
1341 // but we still need to save the draft to the cursor because this is how we restore
1342 // the attachments when the configuration change completes.
1343 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1344 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1345 Toast.LENGTH_LONG).show();
1346 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001347
Mindy Pereira181df782012-03-01 13:32:44 -08001348 // Need to update variables here because the send or save completes
1349 // asynchronously even though the toast shows right away.
1350 discardChanges();
1351 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001352
Mindy Pereira181df782012-03-01 13:32:44 -08001353 // If we are sending, finish the activity
1354 if (!save) {
1355 finish();
1356 }
1357 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001358
Mindy Pereira181df782012-03-01 13:32:44 -08001359 /**
1360 * Save the state of the request messageid map. This allows for the Gmail
1361 * process to be killed, but and still allow for ComposeActivity instances
1362 * to be recreated correctly.
1363 */
1364 private void saveRequestMap() {
1365 // TODO: store the request map in user preferences.
1366 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001367
Mindy Pereira013194c2012-01-06 15:09:33 -08001368 public void doAttach() {
1369 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1370 i.addCategory(Intent.CATEGORY_OPENABLE);
Mindy Pereira181df782012-03-01 13:32:44 -08001371 if (android.provider.Settings.System.getInt(getContentResolver(),
1372 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08001373 i.setType("*/*");
1374 } else {
1375 i.setType("image/*");
1376 }
1377 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08001378 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
1379 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08001380 }
1381
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001382 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001383 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001384 if (mCcBccButton != null) {
1385 mCcBccButton.setVisibility(View.GONE);
1386 }
1387 }
1388
Mindy Pereira326c6602012-01-04 15:32:42 -08001389 @Override
1390 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001391 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08001392 if (position == ComposeActivity.REPLY) {
1393 mComposeMode = ComposeActivity.REPLY;
1394 } else if (position == ComposeActivity.REPLY_ALL) {
1395 mComposeMode = ComposeActivity.REPLY_ALL;
1396 } else if (position == ComposeActivity.FORWARD) {
1397 mComposeMode = ComposeActivity.FORWARD;
1398 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001399 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08001400 resetMessageForModeChange();
Mindy Pereira33fe9082012-01-09 16:24:30 -08001401 initFromRefMessage(mComposeMode, mAccount.name);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001402 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001403 return true;
1404 }
1405
Mindy Pereira154386a2012-01-11 13:02:33 -08001406 private void resetMessageForModeChange() {
1407 // When switching between reply, reply all, forward,
1408 // follow the behavior of webview.
1409 // The contents of the following fields are cleared
1410 // so that they can be populated directly from the
1411 // ref message:
1412 // 1) Any recipient fields
1413 // 2) The subject
1414 mTo.setText("");
1415 mCc.setText("");
1416 mBcc.setText("");
1417 // Any edits to the subject are replaced with the original subject.
1418 mSubject.setText("");
1419
1420 // Any changes to the contents of the following fields are kept:
1421 // 1) Body
1422 // 2) Attachments
1423 // If the user made changes to attachments, keep their changes.
1424 if (!mAttachmentsChanged) {
1425 mAttachmentsView.deleteAllAttachments();
1426 }
1427 }
1428
Mindy Pereira326c6602012-01-04 15:32:42 -08001429 private class ComposeModeAdapter extends ArrayAdapter<String> {
1430
1431 private LayoutInflater mInflater;
1432
1433 public ComposeModeAdapter(Context context) {
1434 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
1435 .getStringArray(R.array.compose_modes));
1436 }
1437
1438 private LayoutInflater getInflater() {
1439 if (mInflater == null) {
1440 mInflater = LayoutInflater.from(getContext());
1441 }
1442 return mInflater;
1443 }
1444
1445 @Override
1446 public View getView(int position, View convertView, ViewGroup parent) {
1447 if (convertView == null) {
1448 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
1449 }
1450 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
1451 return super.getView(position, convertView, parent);
1452 }
1453 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001454
1455 @Override
1456 public void onRespondInline(String text) {
1457 appendToBody(text, false);
1458 }
1459
1460 /**
1461 * Append text to the body of the message. If there is no existing body
1462 * text, just sets the body to text.
1463 *
1464 * @param text
1465 * @param withSignature True to append a signature.
1466 */
1467 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001468 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001469 if (bodyText != null && bodyText.length() > 0) {
1470 bodyText.append(text);
1471 } else {
1472 setBody(text, withSignature);
1473 }
1474 }
1475
1476 /**
1477 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001478 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001479 * @param text
1480 * @param withSignature True to append a signature.
1481 */
1482 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001483 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001484 if (withSignature) {
1485 appendSignature();
1486 }
1487 }
1488
1489 private void appendSignature() {
1490 mSignature = mCachedSettings != null ? mCachedSettings.signature : null;
1491 if (!TextUtils.isEmpty(mSignature)) {
1492 // Appending a signature does not count as changing text.
1493 mBodyView.removeTextChangedListener(this);
1494 mBodyView.append(convertToPrintableSignature(mSignature));
1495 mBodyView.addTextChangedListener(this);
1496 }
1497 }
1498
1499 private String convertToPrintableSignature(String signature) {
1500 String signatureResource = getResources().getString(R.string.signature);
1501 if (signature == null) {
1502 signature = "";
1503 }
1504 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001505 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001506
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001507 @Override
1508 public void onAccountChanged() {
1509 Account selectedAccountInfo = mFromSpinner.getCurrentAccount();
Mindy Pereira181df782012-03-01 13:32:44 -08001510 if (!mAccount.equals(selectedAccountInfo)) {
1511 mAccount = selectedAccountInfo;
1512 mCachedSettings = null;
1513 getLoaderManager().restartLoader(ACCOUNT_SETTINGS_LOADER, null, this);
1514 // TODO: handle discarding attachments when switching accounts.
1515 // Only enable save for this draft if there is any other content
1516 // in the message.
1517 if (!isBlank()) {
1518 enableSave(true);
1519 }
1520 mReplyFromChanged = true;
1521 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001522 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08001523 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001524
1525 public void enableSave(boolean enabled) {
1526 if (mSave != null) {
1527 mSave.setEnabled(enabled);
1528 }
1529 }
1530
1531 public void enableSend(boolean enabled) {
1532 if (mSend != null) {
1533 mSend.setEnabled(enabled);
1534 }
1535 }
1536
1537 /**
1538 * Handles button clicks from any error dialogs dealing with sending
1539 * a message.
1540 */
1541 @Override
1542 public void onClick(DialogInterface dialog, int which) {
1543 switch (which) {
1544 case DialogInterface.BUTTON_POSITIVE: {
1545 doDiscardWithoutConfirmation(true /* show toast */ );
1546 break;
1547 }
1548 case DialogInterface.BUTTON_NEGATIVE: {
1549 // If the user cancels the send, re-enable the send button.
1550 enableSend(true);
1551 break;
1552 }
1553 }
1554
1555 }
1556
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001557 private void doDiscard() {
1558 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
1559 .setPositiveButton(R.string.ok, this)
1560 .setNegativeButton(R.string.cancel, null)
1561 .create().show();
1562 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001563 /**
1564 * Effectively discard the current message.
1565 *
1566 * This method is either invoked from the menu or from the dialog
1567 * once the user has confirmed that they want to discard the message.
1568 * @param showToast show "Message discarded" toast if true
1569 */
1570 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001571 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001572 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
1573 ContentValues values = new ContentValues();
1574 values.put(MessageColumns.SERVER_ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001575 if (mAccount.expungeMessageUri != null) {
1576 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
1577 } else {
1578 // TODO(mindyp): call delete on this conversation instead.
1579 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001580 // This is not strictly necessary (since we should not try to
1581 // save the draft after calling this) but it ensures that if we
1582 // do save again for some reason we make a new draft rather than
1583 // trying to resave an expunged draft.
1584 mDraftId = UIProvider.INVALID_MESSAGE_ID;
1585 }
1586 }
1587
1588 if (showToast) {
1589 // Display a toast to let the user know
1590 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
1591 }
1592
1593 // This prevents the draft from being saved in onPause().
1594 discardChanges();
1595 finish();
1596 }
1597
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001598 private void saveIfNeeded() {
1599 if (mAccount == null) {
1600 // We have not chosen an account yet so there's no way that we can save. This is ok,
1601 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1602 // user has not interacted with us yet and there is no real state to save.
1603 return;
1604 }
1605
1606 if (shouldSave()) {
1607 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
1608 }
1609 }
1610
1611 private void saveIfNeededOnOrientationChanged() {
1612 if (mAccount == null) {
1613 // We have not chosen an account yet so there's no way that we can save. This is ok,
1614 // though, since we are saving our state before AccountsActivity is activated. Thus, the
1615 // user has not interacted with us yet and there is no real state to save.
1616 return;
1617 }
1618
1619 if (shouldSave()) {
1620 doSaveOrientationChanged(!mAddingAttachment /* show toast */, true /* reset IME */);
1621 }
1622 }
1623
1624 /**
1625 * Save a draft if a draft already exists or the message is not empty.
1626 */
1627 public void doSaveOrientationChanged(boolean showToast, boolean resetIME) {
1628 saveOnOrientationChanged();
1629 if (resetIME) {
1630 // Clear the IME composing suggestions from the body.
1631 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1632 }
1633 }
1634
1635 protected boolean saveOnOrientationChanged() {
1636 return sendOrSaveWithSanityChecks(true, false, true);
1637 }
1638
1639 @Override
1640 public void onAttachmentDeleted() {
1641 mAttachmentsChanged = true;
1642 updateSaveUi();
1643 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001644
1645
1646 /**
1647 * This is called any time one of our text fields changes.
1648 */
1649 public void afterTextChanged(Editable s) {
1650 mTextChanged = true;
1651 updateSaveUi();
1652 }
1653
1654 @Override
1655 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1656 // Do nothing.
1657 }
1658
1659 public void onTextChanged(CharSequence s, int start, int before, int count) {
1660 // Do nothing.
1661 }
1662
1663
1664 // There is a big difference between the text associated with an address changing
1665 // to add the display name or to format properly and a recipient being added or deleted.
1666 // Make sure we only notify of changes when a recipient has been added or deleted.
1667 private class RecipientTextWatcher implements TextWatcher {
1668 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
1669
1670 private RecipientEditTextView mView;
1671
1672 private TextWatcher mListener;
1673
1674 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
1675 mView = view;
1676 mListener = listener;
1677 }
1678
1679 @Override
1680 public void afterTextChanged(Editable s) {
1681 if (hasChanged()) {
1682 mListener.afterTextChanged(s);
1683 }
1684 }
1685
1686 private boolean hasChanged() {
1687 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
1688 int totalCount = currRecips.length;
1689 int totalPrevCount = 0;
1690 for (Entry<String, Integer> entry : mContent.entrySet()) {
1691 totalPrevCount += entry.getValue();
1692 }
1693 if (totalCount != totalPrevCount) {
1694 return true;
1695 }
1696
1697 for (String recip : currRecips) {
1698 if (!mContent.containsKey(recip)) {
1699 return true;
1700 } else {
1701 int count = mContent.get(recip) - 1;
1702 if (count < 0) {
1703 return true;
1704 } else {
1705 mContent.put(recip, count);
1706 }
1707 }
1708 }
1709 return false;
1710 }
1711
1712 private String[] tokenizeRecips(String[] recips) {
1713 // Tokenize them all and put them in the list.
1714 String[] recipAddresses = new String[recips.length];
1715 for (int i = 0; i < recips.length; i++) {
1716 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
1717 }
1718 return recipAddresses;
1719 }
1720
1721 @Override
1722 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1723 String[] recips = tokenizeRecips(getAddressesFromList(mView));
1724 for (String recip : recips) {
1725 if (!mContent.containsKey(recip)) {
1726 mContent.put(recip, 1);
1727 } else {
1728 mContent.put(recip, (mContent.get(recip)) + 1);
1729 }
1730 }
1731 }
1732
1733 @Override
1734 public void onTextChanged(CharSequence s, int start, int before, int count) {
1735 // Do nothing.
1736 }
1737 }
Mindy Pereira181df782012-03-01 13:32:44 -08001738
1739 @Override
1740 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
1741 if (id == ACCOUNT_SETTINGS_LOADER) {
1742 if (mAccount.settingsQueryUri != null) {
1743 return new CursorLoader(this, mAccount.settingsQueryUri,
1744 UIProvider.SETTINGS_PROJECTION, null, null, null);
1745 }
1746 }
1747 return null;
1748 }
1749
1750 @Override
1751 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1752 if (loader.getId() == ACCOUNT_SETTINGS_LOADER) {
1753 if (data != null) {
1754 data.moveToFirst();
1755 mCachedSettings = new Settings(data);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001756 appendSignature();
Mindy Pereira181df782012-03-01 13:32:44 -08001757 }
1758 }
1759 }
1760
1761 @Override
1762 public void onLoaderReset(Loader<Cursor> loader) {
1763 // Do nothing.
1764 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001765}