blob: 7615550a781c76590c142cc343e17d4150dc817b [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
Andrew Sappersteinebc4bda2012-08-07 10:53:28 -070019import android.animation.LayoutTransition;
Mindy Pereira326c6602012-01-04 15:32:42 -080020import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070021import android.app.ActionBar.OnNavigationListener;
22import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080023import android.app.ActivityManager;
24import android.app.AlertDialog;
25import android.app.Dialog;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070026import android.app.LoaderManager;
Mindy Pereira6349a042012-01-04 11:25:01 -080027import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080028import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080029import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070030import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080031import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070033import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080034import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080035import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080036import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080037import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080038import android.os.Handler;
39import android.os.HandlerThread;
Paul Westbrookf97588b2012-03-20 11:11:37 -070040import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080041import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080042import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080043import android.text.Html;
44import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080045import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080046import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080047import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080048import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070049import android.view.Gravity;
Mindy Pereira326c6602012-01-04 15:32:42 -080050import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080051import android.view.Menu;
52import android.view.MenuInflater;
53import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080054import android.view.View;
55import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070056import android.view.ViewGroup;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080057import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080058import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080059import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070060import android.widget.EditText;
Mindy Pereira1f936682012-03-02 11:30:33 -080061import android.widget.ImageView;
Mindy Pereira6349a042012-01-04 11:25:01 -080062import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080063import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080064
Mindy Pereirac17d0732011-12-29 10:46:19 -080065import com.android.common.Rfc822Validator;
Andy Huang5c5fd572012-04-08 18:19:29 -070066import com.android.ex.chips.RecipientEditTextView;
67import com.android.mail.R;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080068import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080069import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080070import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080071import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080072import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080073import com.android.mail.providers.Address;
74import com.android.mail.providers.Attachment;
Mindy Pereira5ad02912012-07-09 09:57:18 -070075import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070076import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080077import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080078import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070079import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080080import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080081import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070082import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070083import com.android.mail.providers.UIProvider.DraftType;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -070084import com.android.mail.ui.MailActivity;
Paul Westbrook92227f62012-03-20 10:32:51 -070085import com.android.mail.utils.AccountUtils;
Paul Westbrookb334c902012-06-25 11:42:46 -070086import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -080087import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080088import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080089import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080090import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080091import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080092
Mindy Pereira62de1b12012-04-06 12:17:56 -070093import org.json.JSONException;
94
Mindy Pereira8eca57a2012-03-20 16:42:34 -070095import java.io.UnsupportedEncodingException;
96import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080097import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070098import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080099import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800100import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800101import java.util.HashSet;
102import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700103import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700104import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800105import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800106
107public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800108 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700109 AttachmentDeletedListener, OnAccountChangedListener, LoaderManager.LoaderCallbacks<Cursor> {
Mindy Pereira6349a042012-01-04 11:25:01 -0800110 // Identifiers for which type of composition this is
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700111 static final int COMPOSE = -1;
112 static final int REPLY = 0;
113 static final int REPLY_ALL = 1;
114 static final int FORWARD = 2;
115 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800116
117 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700118 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800119
Mindy Pereira326689d2012-05-17 10:14:14 -0700120 private static final String EXTRA_SHOW_CC = "showCc";
121 private static final String EXTRA_SHOW_BCC = "showBcc";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700122
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700123 private static final String UTF8_ENCODING_NAME = "UTF-8";
124
125 private static final String MAIL_TO = "mailto";
126
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700127 private static final String EXTRA_SUBJECT = "subject";
128
129 private static final String EXTRA_BODY = "body";
130
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700131 private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
132
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700133 // Extra that we can get passed from other activities
134 private static final String EXTRA_TO = "to";
135 private static final String EXTRA_CC = "cc";
136 private static final String EXTRA_BCC = "bcc";
137
138 // List of all the fields
139 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
140
Mindy Pereira82cc5662012-01-09 17:29:30 -0800141 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
142 // Map containing information about requests to create new messages, and the id of the
143 // messages that were the result of those requests.
144 //
145 // This map is used when the activity that initiated the save a of a new message, is killed
146 // before the save has completed (and when we know the id of the newly created message). When
147 // a save is completed, the service that is running in the background, will update the map
148 //
149 // When a new ComposeActivity instance is created, it will attempt to use the information in
150 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
151 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
152 // to populate the map with data stored in shared preferences.
153 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
154 // Key used to store the above map
155 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800156 /**
157 * Notifies the {@code Activity} that the caller is an Email
158 * {@code Activity}, so that the back behavior may be modified accordingly.
159 *
160 * @see #onAppUpPressed
161 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700162 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800163
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700164 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700165
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800166 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700167 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700168 // If this is a reply/forward then this extra will hold a uri we must query
169 // to get the original message.
170 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700171 // If this is an action to edit an existing draft messagge, this extra will hold the
172 // draft message
173 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800174 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700175 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800176 // Request numbers for activities we start
177 private static final int RESULT_PICK_ATTACHMENT = 1;
178 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700179 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700180 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700181
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700182 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
183 private static final String EXTRA_REQUEST_ID = "requestId";
184 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
185 private static final String EXTRA_FOCUS_SELECTION_END = null;
186 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700187 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700188 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800189
Mindy Pereira82cc5662012-01-09 17:29:30 -0800190 /**
191 * A single thread for running tasks in the background.
192 */
193 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800194 private RecipientEditTextView mTo;
195 private RecipientEditTextView mCc;
196 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800197 private Button mCcBccButton;
198 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800199 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800200 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700201 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800202 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800203 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800204 private TextView mSubject;
205
Mindy Pereira326c6602012-01-04 15:32:42 -0800206 private ComposeModeAdapter mComposeModeAdapter;
207 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800208 private boolean mForward;
209 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800210 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700211 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800212 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800213 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800214 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700215 @VisibleForTesting
216 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800217 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800218 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800219 private boolean mTextChanged;
220 private boolean mReplyFromChanged;
221 private MenuItem mSave;
222 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800223 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800224 private AlertDialog mSendConfirmDialog;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700225 @VisibleForTesting
226 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800227 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
228 private Message mDraft;
229 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800230 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800231
Mindy Pereira326c6602012-01-04 15:32:42 -0800232 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700233 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
234 */
235 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700236 private RecipientTextWatcher mToListener;
237 private RecipientTextWatcher mCcListener;
238 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700239 private Uri mRefMessageUri;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700240
241
242 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800243 * Can be called from a non-UI thread.
244 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800245 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700246 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800247 }
248
Mindy Pereira6349a042012-01-04 11:25:01 -0800249 /**
250 * Can be called from a non-UI thread.
251 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800252 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800253 launch(launcher, account, null, COMPOSE);
254 }
255
256 /**
257 * Can be called from a non-UI thread.
258 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800259 public static void reply(Context launcher, Account account, Message message) {
260 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800261 }
262
263 /**
264 * Can be called from a non-UI thread.
265 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800266 public static void replyAll(Context launcher, Account account, Message message) {
267 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800268 }
269
270 /**
271 * Can be called from a non-UI thread.
272 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800273 public static void forward(Context launcher, Account account, Message message) {
274 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800275 }
276
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800277 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800278 Intent intent = new Intent(launcher, ComposeActivity.class);
279 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
280 intent.putExtra(EXTRA_ACTION, action);
281 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700282 if (action == EDIT_DRAFT) {
283 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
284 } else {
285 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
286 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800287 launcher.startActivity(intent);
288 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800289
290 @Override
291 public void onCreate(Bundle savedInstanceState) {
292 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800293 setContentView(R.layout.compose);
294 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800295 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700296 Message message;
Mindy Pereira71c9e562012-05-17 11:01:02 -0700297 boolean showQuotedText = false;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700298 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700299 // Check for any of the possibly supplied accounts.;
300 Account account = null;
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700301 if (hadSavedInstanceStateMessage(savedInstanceState)) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700302 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
303 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
304 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
305 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
306 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700307 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700308 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
309 // Initialize the message from the message in the intent
310 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
311 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700312 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700313 }
Paul Westbrook92227f62012-03-20 10:32:51 -0700314
315 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800316 if (mAccount == null) {
317 return;
318 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700319
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700320 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
321 mLaunchedFromEmail = true;
322 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
323 final Uri dataUri = intent.getData();
324 if (dataUri != null) {
325 final String dataScheme = intent.getData().getScheme();
326 final String accountScheme = mAccount.composeIntentUri.getScheme();
327 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
328 }
329 }
330
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700331 if (mRefMessageUri != null) {
332 // We have a referenced message that we must look up.
333 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
334 return;
335 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700336 initFromDraftMessage(message);
337 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700338 showCcBcc(savedInstanceState);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700339 showQuotedText = message.appendRefMessageContent;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700340 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700341 initFromDraftMessage(message);
Mindy Pereiraef388302012-06-18 19:07:44 -0700342 boolean showBcc = !TextUtils.isEmpty(message.bcc);
343 boolean showCc = showBcc || !TextUtils.isEmpty(message.cc);
344 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700345 // Update the action to the draft type of the previous draft
346 switch (message.draftType) {
347 case UIProvider.DraftType.REPLY:
348 action = REPLY;
349 break;
350 case UIProvider.DraftType.REPLY_ALL:
351 action = REPLY_ALL;
352 break;
353 case UIProvider.DraftType.FORWARD:
354 action = FORWARD;
355 break;
356 case UIProvider.DraftType.COMPOSE:
357 default:
358 action = COMPOSE;
359 break;
360 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700361 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700362 showQuotedText = message.appendRefMessageContent;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700363 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
364 if (mRefMessage != null) {
365 initFromRefMessage(action, mAccount.name);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700366 showQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700367 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700368 } else {
369 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700370 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700371 finishSetup(action, intent, savedInstanceState, showQuotedText);
372 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700373
Mindy Pereira47d0e652012-07-23 09:45:07 -0700374 private Account obtainAccount(Intent intent) {
375 Account account = null;
376 Object accountExtra = null;
377 if (intent != null && intent.getExtras() != null) {
378 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
379 if (accountExtra instanceof Account) {
380 return (Account) accountExtra;
381 }
382 accountExtra = intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
383 }
384 if (account == null) {
385 final String lastAccountUri = MailAppProvider.getInstance().getLastSentFromAccount();
386 if (!TextUtils.isEmpty(lastAccountUri)) {
387 accountExtra = Uri.parse(lastAccountUri);
388 }
389 }
390 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
391 if (syncingAccounts.length > 0) {
392 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
393 // For backwards compatibility, we need to check account
394 // names.
395 for (Account a : syncingAccounts) {
396 if (a.name.equals(accountExtra)) {
397 account = a;
398 }
399 }
400 } else if (accountExtra instanceof Uri) {
401 // The uri of the last viewed account is what is stored in
402 // the current code base.
403 for (Account a : syncingAccounts) {
404 if (a.uri.equals(accountExtra)) {
405 account = a;
406 }
407 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700408 } else {
409 account = syncingAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700410 }
411 }
412 return account;
413 }
414
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700415 private void finishSetup(int action, Intent intent, Bundle savedInstanceState,
416 boolean showQuotedText) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700417 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800418 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800419 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800420 initRecipients();
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700421 // Don't bother with the intent if we have procured a message from the
422 // intent already.
423 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
424 initAttachmentsFromIntent(intent);
425 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800426 initActionBar(action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700427 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
428 action);
Mindy Pereira75f66632012-01-11 11:42:02 -0800429 initChangeListeners();
Mindy Pereira433b1982012-04-03 11:53:07 -0700430 setFocus(action);
Mindy Pereira326689d2012-05-17 10:14:14 -0700431 updateHideOrShowCcBcc();
Mindy Pereira71c9e562012-05-17 11:01:02 -0700432 updateHideOrShowQuotedText(showQuotedText);
433 }
434
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700435 private boolean hadSavedInstanceStateMessage(Bundle savedInstanceState) {
436 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
437 }
438
Mindy Pereira71c9e562012-05-17 11:01:02 -0700439 private void updateHideOrShowQuotedText(boolean showQuotedText) {
440 mQuotedTextView.updateCheckedState(showQuotedText);
Mindy Pereira433b1982012-04-03 11:53:07 -0700441 }
442
443 private void setFocus(int action) {
444 if (action == EDIT_DRAFT) {
445 int type = mDraft.draftType;
446 switch (type) {
447 case UIProvider.DraftType.COMPOSE:
448 case UIProvider.DraftType.FORWARD:
449 action = COMPOSE;
450 break;
451 case UIProvider.DraftType.REPLY:
452 case UIProvider.DraftType.REPLY_ALL:
453 default:
454 action = REPLY;
455 break;
456 }
457 }
458 switch (action) {
459 case FORWARD:
460 case COMPOSE:
461 mTo.requestFocus();
462 break;
463 case REPLY:
464 case REPLY_ALL:
465 default:
466 focusBody();
467 break;
468 }
469 }
470
471 /**
472 * Focus the body of the message.
473 */
474 public void focusBody() {
475 mBodyView.requestFocus();
476 int length = mBodyView.getText().length();
477
478 int signatureStartPos = getSignatureStartPosition(
479 mSignature, mBodyView.getText().toString());
480 if (signatureStartPos > -1) {
481 // In case the user deleted the newlines...
482 mBodyView.setSelection(signatureStartPos);
483 } else if (length > 0) {
484 // Move cursor to the end.
485 mBodyView.setSelection(length);
486 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800487 }
488
489 @Override
490 protected void onResume() {
491 super.onResume();
492 // Update the from spinner as other accounts
493 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800494 if (mFromSpinner != null && mAccount != null) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700495 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount);
Mindy Pereira818143e2012-01-11 13:59:49 -0800496 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800497 }
498
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800499 @Override
500 protected void onPause() {
501 super.onPause();
502
503 if (mSendConfirmDialog != null) {
504 mSendConfirmDialog.dismiss();
505 }
506 if (mRecipientErrorDialog != null) {
507 mRecipientErrorDialog.dismiss();
508 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700509 // When the user exits the compose view, see if this draft needs saving.
510 if (isFinishing()) {
511 saveIfNeeded();
512 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800513 }
514
515 @Override
516 protected final void onActivityResult(int request, int result, Intent data) {
517 mAddingAttachment = false;
518
519 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
520 addAttachmentAndUpdateView(data);
521 }
522 }
523
524 @Override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700525 public final void onRestoreInstanceState(Bundle savedInstanceState) {
526 super.onRestoreInstanceState(savedInstanceState);
527 if (savedInstanceState != null) {
528 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
529 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
530 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
531 // There should be a focus and it should be an EditText since we
532 // only save these extras if these conditions are true.
533 EditText focusEditText = (EditText) getCurrentFocus();
534 final int length = focusEditText.getText().length();
535 if (selectionStart < length && selectionEnd < length) {
536 focusEditText.setSelection(selectionStart, selectionEnd);
537 }
538 }
539 }
540 }
541
542 @Override
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800543 public final void onSaveInstanceState(Bundle state) {
544 super.onSaveInstanceState(state);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700545 // The framework is happy to save and restore the selection but only if it also saves and
546 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
547 // this manually.
548 View focus = getCurrentFocus();
549 if (focus != null && focus instanceof EditText) {
550 EditText focusEditText = (EditText) focus;
551 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
552 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
553 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700554
555 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700556 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700557 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
558 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
559 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700560 if (selectedReplyFromAccount != null) {
561 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
562 .toString());
563 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
564 } else {
565 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
566 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800567
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700568 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
569 // We don't have a draft id, and we have a request id,
570 // save the request id.
571 state.putInt(EXTRA_REQUEST_ID, mRequestId);
572 }
573
574 // We want to restore the current mode after a pause
575 // or rotation.
576 int mode = getMode();
577 state.putInt(EXTRA_ACTION, mode);
578
579 Message message = createMessage(selectedReplyFromAccount, mode);
580 state.putParcelable(EXTRA_MESSAGE, message);
581
582 if (mRefMessage != null) {
583 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
584 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700585 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
586 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700587 }
588
589 private int getMode() {
590 int mode = ComposeActivity.COMPOSE;
591 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700592 if (actionBar != null
593 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700594 mode = actionBar.getSelectedNavigationIndex();
595 }
596 return mode;
597 }
598
599 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
600 Message message = new Message();
601 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -0700602 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700603 message.uri = null;
604 message.conversationUri = null;
605 message.subject = mSubject.getText().toString();
606 message.snippet = null;
Mindy Pereira1eedc752012-07-16 16:34:41 -0700607 message.from = selectedReplyFromAccount != null ? selectedReplyFromAccount.address
Mindy Pereirab67aa8f2012-07-11 15:09:06 -0700608 : mAccount != null ? mAccount.name : null;
Mindy Pereira3c911582012-08-09 16:59:09 -0700609 message.to = formatSenders(mTo.getText().toString());
610 message.cc = formatSenders(mCc.getText().toString());
611 message.bcc = formatSenders(mBcc.getText().toString());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700612 message.replyTo = null;
613 message.dateReceivedMs = 0;
614 String htmlBody = Html.toHtml(mBodyView.getText());
615 StringBuilder fullBody = new StringBuilder(htmlBody);
616 message.bodyHtml = fullBody.toString();
617 message.bodyText = mBodyView.getText().toString();
618 message.embedsExternalResources = false;
619 message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
Mindy Pereirad2bef8b2012-05-30 12:14:52 -0700620 message.draftType = getDraftType(mode);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700621 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
622 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
623 message.hasAttachments = attachments != null && attachments.size() > 0;
624 message.attachmentListUri = null;
625 message.messageFlags = 0;
626 message.saveUri = null;
627 message.sendUri = null;
628 message.alwaysShowImages = false;
629 message.attachmentsJson = Attachment.toJSONArray(attachments);
630 CharSequence quotedText = mQuotedTextView.getQuotedText();
631 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
632 .getQuotedTextOffset(quotedText.toString()) : -1;
633 message.accountUri = null;
634 return message;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800635 }
636
Mindy Pereira3c911582012-08-09 16:59:09 -0700637 private String formatSenders(String string) {
638 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
639 return string.substring(0, string.length() - 1);
640 }
641 return string;
642 }
643
Mindy Pereira818143e2012-01-11 13:59:49 -0800644 @VisibleForTesting
645 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700646 if (account == null) {
647 return;
648 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700649 if (!account.equals(mAccount)) {
650 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700651 mCachedSettings = mAccount.settings;
652 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700653 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700654 if (mAccount != null) {
655 MailActivity.setForegroundNdef(MailActivity.getMailtoNdef(mAccount.name));
656 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800657 }
658
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700659 private void initFromSpinner(Bundle bundle, int action) {
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700660 String accountString = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700661 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700662 action = COMPOSE;
663 }
664 mFromSpinner.asyncInitFromSpinner(action, mAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700665 if (bundle != null) {
666 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
667 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
668 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
669 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
670 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
671 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
672 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700673 }
674 if (mReplyFromAccount == null) {
675 if (mDraft != null) {
676 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
677 } else if (mRefMessage != null) {
678 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
679 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700680 }
681 if (mReplyFromAccount == null) {
682 mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700683 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700684 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700685
Mindy Pereira62de1b12012-04-06 12:17:56 -0700686 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700687
Mindy Pereira62de1b12012-04-06 12:17:56 -0700688 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700689 // If there is only 1 account, just show that account.
690 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700691 // send mail from / save drafts to.
692 mFromStatic.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700693 mFromStaticText.setText(mAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700694 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700695 } else {
696 mFromStatic.setVisibility(View.VISIBLE);
697 mFromStaticText.setText(mAccount.name);
698 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700699 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800700 }
701
Mindy Pereira62de1b12012-04-06 12:17:56 -0700702 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
703 if (refMessage.accountUri != null) {
704 // This must be from combined inbox.
705 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
706 for (ReplyFromAccount from : replyFromAccounts) {
707 if (from.account.uri.equals(refMessage.accountUri)) {
708 return from;
709 }
710 }
711 return null;
712 } else {
713 return getReplyFromAccount(account, refMessage);
714 }
715 }
716
717 /**
718 * Given an account and which email address the message was sent to,
719 * return who the message should be sent from.
720 * @param account Account in which the message arrived.
721 * @param sentTo Email address to which the message was sent.
722 * @return the address from which to reply.
723 */
724 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
725 // First see if we are supposed to use the default address or
726 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -0700727 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700728 return getDefaultReplyFromAccount(account);
729 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -0700730 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -0700731 // all the message recipients and find one that matches
732 // a custom from or account.
733 List<String> allRecipients = new ArrayList<String>();
734 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.to)));
735 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.cc)));
736 return getMatchingRecipient(account, allRecipients);
737 }
738 }
739
740 /**
741 * Compare all the recipients of an email to the current account and all
742 * custom addresses associated with that account. Return the match if there
743 * is one, or the default account if there isn't.
744 */
745 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
746 // Tokenize the list and place in a hashmap.
747 ReplyFromAccount matchingReplyFrom = null;
748 Rfc822Token[] tokens;
749 HashSet<String> recipientsMap = new HashSet<String>();
750 for (String address : sentTo) {
751 tokens = Rfc822Tokenizer.tokenize(address);
752 for (int i = 0; i < tokens.length; i++) {
753 recipientsMap.add(tokens[i].getAddress());
754 }
755 }
756
757 int matchingAddressCount = 0;
758 List<ReplyFromAccount> customFroms;
759 try {
760 customFroms = FromAddressSpinner.getAccountSpecificFroms(account);
761 if (customFroms != null) {
762 for (ReplyFromAccount entry : customFroms) {
763 if (recipientsMap.contains(entry.address)) {
764 matchingReplyFrom = entry;
765 matchingAddressCount++;
766 }
767 }
768 }
769 } catch (JSONException e) {
770 LogUtils.wtf(LOG_TAG, "Exception parsing from addresses for account %s",
771 account.name);
772 }
773 if (matchingAddressCount > 1) {
774 matchingReplyFrom = getDefaultReplyFromAccount(account);
775 }
776 return matchingReplyFrom;
777 }
778
779 private ReplyFromAccount getDefaultReplyFromAccount(Account account) {
780 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
781 for (ReplyFromAccount from : replyFromAccounts) {
782 if (from.isDefault) {
783 return from;
784 }
785 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700786 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
787 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700788 }
789
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700790 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
791 String sender = msg.from;
Mindy Pereira62de1b12012-04-06 12:17:56 -0700792 ReplyFromAccount replyFromAccount = null;
793 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
794 if (TextUtils.equals(account.name, sender)) {
795 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700796 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700797 } else {
798 for (ReplyFromAccount fromAccount : replyFromAccounts) {
799 if (TextUtils.equals(fromAccount.name, sender)) {
800 replyFromAccount = fromAccount;
801 break;
802 }
803 }
804 }
805 return replyFromAccount;
806 }
807
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800808 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800809 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800810 if (mCcBccButton != null) {
811 mCcBccButton.setOnClickListener(this);
812 }
813 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800814 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Andrew Sappersteinca87d6d2012-08-08 16:14:52 -0700815 LayoutTransition transition =
816 ((ViewGroup) findViewById(R.id.content)).getLayoutTransition();
817 mAttachmentsView.setComposeLayoutTransition(transition);
Mindy Pereira1f936682012-03-02 11:30:33 -0800818 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
819 if (mAttachmentsButton != null) {
820 mAttachmentsButton.setOnClickListener(this);
821 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800822 mTo = (RecipientEditTextView) findViewById(R.id.to);
823 mCc = (RecipientEditTextView) findViewById(R.id.cc);
824 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800825 // TODO: add special chips text change watchers before adding
826 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800827 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800828 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
829 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -0700830 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800831 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800832 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800833 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800834 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800835 }
836
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700837 protected TextView getBody() {
838 return mBodyView;
839 }
840
841 @VisibleForTesting
842 public Account getFromAccount() {
843 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
844 mReplyFromAccount.account : mAccount;
845 }
846
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700847 private void clearChangeListeners() {
848 mSubject.removeTextChangedListener(this);
849 mBodyView.removeTextChangedListener(this);
850 mTo.removeTextChangedListener(mToListener);
851 mCc.removeTextChangedListener(mCcListener);
852 mBcc.removeTextChangedListener(mBccListener);
853 mFromSpinner.setOnAccountChangedListener(null);
854 mAttachmentsView.setAttachmentChangesListener(null);
855 }
856
Mindy Pereira75f66632012-01-11 11:42:02 -0800857 // Now that the message has been initialized from any existing draft or
858 // ref message data, set up listeners for any changes that occur to the
859 // message.
860 private void initChangeListeners() {
861 mSubject.addTextChangedListener(this);
862 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700863 if (mToListener == null) {
864 mToListener = new RecipientTextWatcher(mTo, this);
865 }
866 mTo.addTextChangedListener(mToListener);
867 if (mCcListener == null) {
868 mCcListener = new RecipientTextWatcher(mCc, this);
869 }
870 mCc.addTextChangedListener(mCcListener);
871 if (mBccListener == null) {
872 mBccListener = new RecipientTextWatcher(mBcc, this);
873 }
874 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -0800875 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800876 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800877 }
878
Mindy Pereira326c6602012-01-04 15:32:42 -0800879 private void initActionBar(int action) {
880 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800881 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700882 if (actionBar == null) {
883 return;
884 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800885 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800886 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
887 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800888 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800889 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800890 if (mComposeModeAdapter == null) {
891 mComposeModeAdapter = new ComposeModeAdapter(this);
892 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800893 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
894 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800895 switch (action) {
896 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800897 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800898 break;
899 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800900 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800901 break;
902 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800903 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800904 break;
905 }
906 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700907 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
908 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
909 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800910 }
911
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800912 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700913 setFieldsFromRefMessage(action, recipientAddress);
914 if (mRefMessage != null) {
915 // CC field only gets populated when doing REPLY_ALL.
916 // BCC never gets auto-populated, unless the user is editing
917 // a draft with one.
Mindy Pereira29a717e2012-07-25 18:05:48 -0700918 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700919 mCcBccView.show(false, true, false);
920 }
921 }
922 updateHideOrShowCcBcc();
923 }
924
925 private void setFieldsFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700926 setSubject(mRefMessage, action);
927 // Setup recipients
928 if (action == FORWARD) {
929 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800930 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700931 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700932 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700933 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
934 initAttachments(mRefMessage);
935 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800936 }
937
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700938 private void initFromDraftMessage(Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700939 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
940
941 mDraft = message;
942 mDraftId = message.id;
943 mSubject.setText(message.subject);
944 mForward = message.draftType == UIProvider.DraftType.FORWARD;
945 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
946 addToAddresses(toAddresses);
947 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
948 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -0700949 if (message.hasAttachments) {
950 List<Attachment> attachments = message.getAttachments();
951 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -0700952 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -0700953 }
954 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -0700955 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -0700956 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700957 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -0700958 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700959 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -0700960 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -0700961 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -0700962 // Find the offset in the htmltext of the actual quoted text and strip it out.
963 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
964 if (quotedTextIndex > -1) {
965 htmlText = Html.fromHtml(message.bodyHtml.substring(0, quotedTextIndex));
966 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
967 message.bodyHtml.length());
968 }
Mindy Pereira002ff522012-05-30 10:31:26 -0700969 }
970 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700971 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -0700972 final String body = message.bodyText;
973 final CharSequence bodyText = !TextUtils.isEmpty(body) ?
974 (quotedTextIndex > -1 ?
975 message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
976 : "";
Mindy Pereira002ff522012-05-30 10:31:26 -0700977 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -0700978 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
979 : null;
Mindy Pereira002ff522012-05-30 10:31:26 -0700980 }
981 mBodyView.setText(bodyText);
982 }
983 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -0700984 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700985 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700986 }
987
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700988 /**
989 * Fill all the widgets with the content found in the Intent Extra, if any.
990 * Also apply the same style to all widgets. Note: if initFromExtras is
991 * called as a result of switching between reply, reply all, and forward per
992 * the latest revision of Gmail, and the user has already made changes to
993 * attachments on a previous incarnation of the message (as a reply, reply
994 * all, or forward), the original attachments from the message will not be
995 * re-instantiated. The user's changes will be respected. This follows the
996 * web gmail interaction.
997 */
998 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700999 // If we were invoked with a SENDTO intent, the value
1000 // should take precedence
1001 final Uri dataUri = intent.getData();
1002 if (dataUri != null) {
1003 if (MAIL_TO.equals(dataUri.getScheme())) {
1004 initFromMailTo(dataUri.toString());
1005 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001006 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001007 String toText = dataUri.getSchemeSpecificPart();
1008 if (toText != null) {
1009 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001010 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001011 }
1012 }
1013 }
1014 }
1015
1016 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1017 if (extraStrings != null) {
1018 addToAddresses(Arrays.asList(extraStrings));
1019 }
1020 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1021 if (extraStrings != null) {
1022 addCcAddresses(Arrays.asList(extraStrings), null);
1023 }
1024 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1025 if (extraStrings != null) {
1026 addBccAddresses(Arrays.asList(extraStrings));
1027 }
1028
1029 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1030 if (extraString != null) {
1031 mSubject.setText(extraString);
1032 }
1033
1034 for (String extra : ALL_EXTRAS) {
1035 if (intent.hasExtra(extra)) {
1036 String value = intent.getStringExtra(extra);
1037 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001038 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001039 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001040 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001041 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001042 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001043 } else if (EXTRA_SUBJECT.equals(extra)) {
1044 mSubject.setText(value);
1045 } else if (EXTRA_BODY.equals(extra)) {
1046 setBody(value, true /* with signature */);
1047 }
1048 }
1049 }
1050
1051 Bundle extras = intent.getExtras();
1052 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001053 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1054 if (text != null) {
1055 setBody(text, true /* with signature */);
1056 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001057 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001058 }
1059
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001060 @VisibleForTesting
1061 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001062 // TODO: handle the case where there are spaces in the display name as
1063 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1064 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001065 // Since URLDecode.decode changes + into ' ', and + is a valid
1066 // email character, we need to find/ replace these ourselves before
1067 // decoding.
1068 String replacePlus = s.replace("+", "%2B");
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001069 try {
1070 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
1071 } catch (IllegalArgumentException e) {
1072 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1073 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1074 } else {
1075 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1076 }
1077 return null;
1078 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001079 }
1080
1081 /**
1082 * Initialize the compose view from a String representing a mailTo uri.
1083 * @param mailToString The uri as a string.
1084 */
1085 public void initFromMailTo(String mailToString) {
1086 // We need to disguise this string as a URI in order to parse it
1087 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1088 Uri uri = Uri.parse("foo://" + mailToString);
1089 int index = mailToString.indexOf("?");
1090 int length = "mailto".length() + 1;
1091 String to;
1092 try {
1093 // Extract the recipient after mailto:
1094 if (index == -1) {
1095 to = decodeEmailInUri(mailToString.substring(length));
1096 } else {
1097 to = decodeEmailInUri(mailToString.substring(length, index));
1098 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001099 if (!TextUtils.isEmpty(to)) {
1100 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1101 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001102 } catch (UnsupportedEncodingException e) {
1103 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1104 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1105 } else {
1106 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1107 }
1108 }
1109
1110 List<String> cc = uri.getQueryParameters("cc");
1111 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1112
1113 List<String> otherTo = uri.getQueryParameters("to");
1114 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1115
1116 List<String> bcc = uri.getQueryParameters("bcc");
1117 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1118
1119 List<String> subject = uri.getQueryParameters("subject");
1120 if (subject.size() > 0) {
1121 try {
1122 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
1123 } catch (UnsupportedEncodingException e) {
1124 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1125 e.getMessage(), subject);
1126 }
1127 }
1128
1129 List<String> body = uri.getQueryParameters("body");
1130 if (body.size() > 0) {
1131 try {
1132 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
1133 true /* with signature */);
1134 } catch (UnsupportedEncodingException e) {
1135 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1136 }
1137 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001138 }
1139
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001140 @VisibleForTesting
1141 protected void initAttachments(Message refMessage) {
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001142 try {
1143 mAttachmentsView.addAttachments(mAccount, refMessage);
1144 } catch (AttachmentFailureException e) {
1145 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1146 showAttachmentTooBigToast();
1147 }
1148 }
1149
1150 /**
1151 * When an attachment is too large to be added to a message, show a toast.
1152 * This method also updates the position of the toast so that it is shown
1153 * clearly above they keyboard if it happens to be open.
1154 */
1155 private void showAttachmentTooBigToast() {
1156 showErrorToast(R.string.too_large_to_attach);
1157 }
1158
1159 private void showErrorToast(int resId) {
1160 Toast t = Toast.makeText(this, resId, Toast.LENGTH_LONG);
1161 t.setText(resId);
1162 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1163 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1164 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001165 }
1166
Paul Westbrookf97588b2012-03-20 11:11:37 -07001167 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001168 Bundle extras = intent.getExtras();
1169 if (extras == null) {
1170 extras = Bundle.EMPTY;
1171 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001172 final String action = intent.getAction();
1173 if (!mAttachmentsChanged) {
1174 long totalSize = 0;
1175 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1176 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1177 for (String uriString : uris) {
1178 final Uri uri = Uri.parse(uriString);
1179 long size = 0;
1180 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001181 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001182 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001183 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001184 showAttachmentTooBigToast();
Paul Westbrookf97588b2012-03-20 11:11:37 -07001185 }
1186 totalSize += size;
1187 }
1188 }
1189 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
1190 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1191 long size = 0;
1192 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001193 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001194 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001195 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001196 showAttachmentTooBigToast();
Paul Westbrookf97588b2012-03-20 11:11:37 -07001197 }
1198 totalSize += size;
1199 }
1200
1201 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
1202 && extras.containsKey(Intent.EXTRA_STREAM)) {
1203 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
1204 for (Parcelable uri : uris) {
1205 long size = 0;
1206 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001207 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001208 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001209 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001210 showAttachmentTooBigToast();
Paul Westbrookf97588b2012-03-20 11:11:37 -07001211 }
1212 totalSize += size;
1213 }
1214 }
1215
1216 if (totalSize > 0) {
1217 mAttachmentsChanged = true;
1218 updateSaveUi();
1219 }
1220 }
1221 }
1222
1223
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001224 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1225 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001226 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1227 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001228 }
1229
1230 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001231 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001232 boolean ccVisible = mCcBccView.isCcVisible();
1233 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001234 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001235 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001236 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001237 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001238 : R.string.add_bcc_label));
1239 } else {
1240 mCcBccButton.setVisibility(View.GONE);
1241 }
1242 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001243 }
1244
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001245 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001246 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1247 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1248 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1249 if (showCc || showBcc) {
1250 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001251 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001252 }
1253 }
1254
Mindy Pereira013194c2012-01-06 15:09:33 -08001255 /**
1256 * Add attachment and update the compose area appropriately.
1257 * @param data
1258 */
1259 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001260 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1261 }
1262
Andy Huang5c5fd572012-04-08 18:19:29 -07001263 public void addAttachmentAndUpdateView(Uri contentUri) {
1264 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001265 return;
1266 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001267 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001268 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1269 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001270 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001271 showErrorToast(R.string.generic_attachment_problem);
Andy Huang5c5fd572012-04-08 18:19:29 -07001272 }
1273 }
1274
1275 public void addAttachmentAndUpdateView(Attachment attachment) {
1276 try {
1277 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001278 if (size > 0) {
1279 mAttachmentsChanged = true;
1280 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001281 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001282 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001283 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001284 showAttachmentTooBigToast();
Mindy Pereira013194c2012-01-06 15:09:33 -08001285 }
1286 }
1287
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001288 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001289 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001290 // Don't populate the address if this is a forward.
1291 if (action == ComposeActivity.FORWARD) {
1292 return;
1293 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001294 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001295 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001296
Mindy Pereira818143e2012-01-11 13:59:49 -08001297 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001298 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001299 // This is the email address of the current user, i.e. the one composing
1300 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001301 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001302 String fromAddress = getAddress(refMessage.from);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001303 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
1304 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001305 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001306
1307 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1308 // Cc list is the union of the To and Cc recipients of the original
1309 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001310 // already on the To list.
1311 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001312 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001313 sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001314 addToAddresses(toAddresses);
1315 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001316 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001317 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
Mindy Pereira9b18a9a2012-07-25 18:13:57 -07001318 sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001319 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001320 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001321 addRecipients(accountEmail, ccAddresses,
1322 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001323 addCcAddresses(ccAddresses, toAddresses);
1324 }
1325 }
1326
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001327 private String getAddress(String toParse) {
1328 if (!TextUtils.isEmpty(toParse)) {
1329 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(toParse);
1330 if (tokens.length > 0) {
1331 return tokens[0].getAddress();
1332 }
1333 }
1334 return "";
1335 }
1336
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001337 private void addToAddresses(Collection<String> addresses) {
1338 addAddressesToList(addresses, mTo);
1339 }
1340
1341 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001342 addCcAddressesToList(tokenizeAddressList(addresses),
1343 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001344 }
1345
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001346 private void addBccAddresses(Collection<String> addresses) {
1347 addAddressesToList(addresses, mBcc);
1348 }
1349
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001350 @VisibleForTesting
1351 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1352 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1353 String address;
1354
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001355 if (compareToList == null) {
1356 for (Rfc822Token[] tokens : addresses) {
1357 for (int i = 0; i < tokens.length; i++) {
1358 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001359 list.append(address + END_TOKEN);
1360 }
1361 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001362 } else {
1363 HashSet<String> compareTo = convertToHashSet(compareToList);
1364 for (Rfc822Token[] tokens : addresses) {
1365 for (int i = 0; i < tokens.length; i++) {
1366 address = tokens[i].toString();
1367 // Check if this is a duplicate:
1368 if (!compareTo.contains(tokens[i].getAddress())) {
1369 // Get the address here
1370 list.append(address + END_TOKEN);
1371 }
1372 }
1373 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001374 }
1375 }
1376
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001377 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
1378 HashSet<String> hash = new HashSet<String>();
1379 for (Rfc822Token[] tokens : list) {
1380 for (int i = 0; i < tokens.length; i++) {
1381 hash.add(tokens[i].getAddress());
1382 }
1383 }
1384 return hash;
1385 }
1386
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001387 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1388 @VisibleForTesting
1389 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1390
1391 for (String address: addresses) {
1392 tokenized.add(Rfc822Tokenizer.tokenize(address));
1393 }
1394 return tokenized;
1395 }
1396
1397 @VisibleForTesting
1398 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1399 for (String address : addresses) {
1400 addAddressToList(address, list);
1401 }
1402 }
1403
1404 private void addAddressToList(String address, RecipientEditTextView list) {
1405 if (address == null || list == null)
1406 return;
1407
1408 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1409
1410 for (int i = 0; i < tokens.length; i++) {
1411 list.append(tokens[i] + END_TOKEN);
1412 }
1413 }
1414
1415 @VisibleForTesting
1416 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001417 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001418 // The To recipient is the reply-to address specified in the original
1419 // message, unless it is:
1420 // the current user OR a custom from of the current user, in which case
1421 // it's the To recipient list of the original message.
1422 // OR missing, in which case use the sender of the original message
1423 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001424 if (!TextUtils.isEmpty(replyToAddress)) {
1425 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001426 } else {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001427 if (!recipientMatchesThisAccount(account, senderAddress)) {
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001428 toAddresses.add(senderAddress);
1429 } else {
1430 // This happens if the user replies to a message they originally
Mindy Pereira1883b342012-06-20 08:34:56 -07001431 // wrote. In this case, "reply" really means "re-send," so we
1432 // target the original recipients. This works as expected even
1433 // if the user sent the original message to themselves.
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001434 toAddresses.addAll(Arrays.asList(inToAddresses));
1435 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001436 }
1437 return toAddresses;
1438 }
1439
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001440 private void addRecipients(String accountAddress, Set<String> recipients, String[] addresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001441 for (String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001442 // Do not add this account, or any of its custom from addresses, to
1443 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001444 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001445 if (!recipientMatchesThisAccount(accountAddress, recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001446 recipients.add(email.replace("\"\"", ""));
1447 }
1448 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001449 }
1450
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001451 /**
1452 * A recipient matches this account if it has the same address as the
1453 * currently selected account OR one of the custom from addresses associated
1454 * with the currently selected account.
1455 * @param accountAddress currently selected account
1456 * @param recipientAddress address we are comparing with the currently selected account
1457 * @return
1458 */
1459 protected boolean recipientMatchesThisAccount(String accountAddress, String recipientAddress) {
1460 return accountAddress.equalsIgnoreCase(recipientAddress)
1461 || ReplyFromAccount.isCustomFrom(recipientAddress,
1462 mFromSpinner.getReplyFromAccounts());
1463 }
1464
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001465 private void setSubject(Message refMessage, int action) {
1466 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001467 String prefix;
1468 String correctedSubject = null;
1469 if (action == ComposeActivity.COMPOSE) {
1470 prefix = "";
1471 } else if (action == ComposeActivity.FORWARD) {
1472 prefix = getString(R.string.forward_subject_label);
1473 } else {
1474 prefix = getString(R.string.reply_subject_label);
1475 }
1476
1477 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07001478 if (!TextUtils.isEmpty(subject)
1479 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001480 correctedSubject = subject;
1481 } else {
1482 correctedSubject = String
1483 .format(getString(R.string.formatted_subject), prefix, subject);
1484 }
1485 mSubject.setText(correctedSubject);
1486 }
1487
Mindy Pereira818143e2012-01-11 13:59:49 -08001488 private void initRecipients() {
1489 setupRecipients(mTo);
1490 setupRecipients(mCc);
1491 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001492 }
1493
Mindy Pereira818143e2012-01-11 13:59:49 -08001494 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001495 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -08001496 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001497 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001498 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001499 int offset = accountName.indexOf("@") + 1;
1500 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001501 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001502 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001503 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001504 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001505 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001506 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001507 }
1508
1509 @Override
1510 public void onClick(View v) {
1511 int id = v.getId();
1512 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001513 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001514 // Verify that cc/ bcc aren't showing.
1515 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001516 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001517 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001518 case R.id.add_attachment:
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07001519 openAttachmentTypeSelectionDialog();
Mindy Pereira1f936682012-03-02 11:30:33 -08001520 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001521 }
1522 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001523
1524 @Override
1525 public boolean onCreateOptionsMenu(Menu menu) {
1526 super.onCreateOptionsMenu(menu);
1527 MenuInflater inflater = getMenuInflater();
1528 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001529 mSave = menu.findItem(R.id.save);
1530 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001531 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1532 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1533 if (helpItem != null) {
1534 helpItem.setVisible(mAccount != null
1535 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1536 }
1537 if (sendFeedbackItem != null) {
1538 sendFeedbackItem.setVisible(mAccount != null
1539 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1540 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001541 return true;
1542 }
1543
1544 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001545 public boolean onPrepareOptionsMenu(Menu menu) {
1546 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001547 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001548 // Its possible there is a menu item OR a button.
1549 boolean ccFieldVisible = mCc.isShown();
1550 boolean bccFieldVisible = mBcc.isShown();
1551 if (!ccFieldVisible || !bccFieldVisible) {
1552 ccBcc.setVisible(true);
1553 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1554 : R.string.add_bcc_label));
1555 } else {
1556 ccBcc.setVisible(false);
1557 }
1558 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001559 if (mSave != null) {
1560 mSave.setEnabled(shouldSave());
1561 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001562 return true;
1563 }
1564
1565 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001566 public boolean onOptionsItemSelected(MenuItem item) {
1567 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001568 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001569 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001570 case R.id.add_attachment:
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07001571 openAttachmentTypeSelectionDialog();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001572 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001573 case R.id.add_cc_bcc:
1574 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001575 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001576 case R.id.save:
Mindy Pereira48e31b02012-05-30 13:12:24 -07001577 doSave(true);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001578 break;
1579 case R.id.send:
1580 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001581 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001582 case R.id.discard:
1583 doDiscard();
1584 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001585 case R.id.settings:
1586 Utils.showSettings(this, mAccount);
1587 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001588 case android.R.id.home:
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001589 onAppUpPressed();
Mindy Pereirafbe40192012-03-20 10:40:45 -07001590 break;
1591 case R.id.help_info_menu_item:
1592 // TODO: enable context sensitive help
Paul Westbrook498e76d2012-04-12 16:33:02 -07001593 Utils.showHelp(this, mAccount, null);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001594 break;
1595 case R.id.feedback_menu_item:
1596 Utils.sendFeedback(this, mAccount);
1597 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001598 default:
1599 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001600 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001601 }
1602 return !handled ? super.onOptionsItemSelected(item) : handled;
1603 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001604
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001605 private void onAppUpPressed() {
1606 if (mLaunchedFromEmail) {
1607 // If this was started from Gmail, simply treat app up as the system back button, so
1608 // that the last view is restored.
1609 onBackPressed();
1610 return;
1611 }
1612
1613 // Fire the main activity to ensure it launches the "top" screen of mail.
1614 // Since the main Activity is singleTask, it should revive that task if it was already
1615 // started.
Mindy Pereira5ad02912012-07-09 09:57:18 -07001616 Folder defaultInbox = new Folder();
1617 defaultInbox.uri = mAccount.settings.defaultInbox;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001618 final Intent mailIntent =
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001619 Utils.createViewFolderIntent(defaultInbox, mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001620
1621 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
1622 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
1623 startActivity(mailIntent);
1624 finish();
1625 }
1626
Mindy Pereira33fe9082012-01-09 16:24:30 -08001627 private void doSend() {
Mindy Pereira8aa913b2012-07-17 10:58:39 -07001628 clearImeText();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001629 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001630 }
1631
Mindy Pereira48e31b02012-05-30 13:12:24 -07001632 private void doSave(boolean showToast) {
1633 // Clear the IME composing suggestions from the body and subject before saving.
Mindy Pereira8aa913b2012-07-17 10:58:39 -07001634 clearImeText();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001635 sendOrSaveWithSanityChecks(true, showToast, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07001636 }
1637
Mindy Pereira8aa913b2012-07-17 10:58:39 -07001638 private void clearImeText() {
1639 mBodyView.clearComposingText();
1640 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1641 mSubject.clearComposingText();
1642 BaseInputConnection.removeComposingSpans(mSubject.getEditableText());
Mindy Pereira33fe9082012-01-09 16:24:30 -08001643 }
1644
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001645 @VisibleForTesting
1646 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001647 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001648 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1649 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001650 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1651 }
1652
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001653 @VisibleForTesting
1654 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001655 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001656 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001657 public final SendOrSaveCallback mSendOrSaveCallback;
1658 @VisibleForTesting
1659 public final SendOrSaveMessage mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001660
1661 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1662 SendOrSaveCallback callback) {
1663 mContext = context;
1664 mSendOrSaveCallback = callback;
1665 mSendOrSaveMessage = message;
1666 }
1667
1668 @Override
1669 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001670 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001671
Mindy Pereira92551d02012-04-05 11:31:12 -07001672 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001673 Message message = mSendOrSaveCallback.getMessage();
1674 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001675 // If a previous draft has been saved, in an account that is different
1676 // than what the user wants to send from, remove the old draft, and treat this
1677 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001678 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001679 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1680 ContentResolver resolver = mContext.getContentResolver();
1681 ContentValues values = new ContentValues();
1682 values.put(BaseColumns._ID, messageId);
Mindy Pereira92551d02012-04-05 11:31:12 -07001683 if (selectedAccount.account.expungeMessageUri != null) {
1684 resolver.update(selectedAccount.account.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001685 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001686 } else {
1687 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001688 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001689 // reset messageId to 0, so a new message will be created
1690 messageId = UIProvider.INVALID_MESSAGE_ID;
1691 }
1692 }
1693
1694 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001695 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001696 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001697 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001698 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1699 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001700 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001701 ContentResolver resolver = mContext.getContentResolver();
Mindy Pereira92551d02012-04-05 11:31:12 -07001702 Uri messageUri = resolver
1703 .insert(sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
1704 : selectedAccount.account.sendMessageUri,
1705 sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001706 if (sendOrSaveMessage.mSave && messageUri != null) {
1707 Cursor messageCursor = resolver.query(messageUri,
1708 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001709 if (messageCursor != null) {
1710 try {
1711 if (messageCursor.moveToFirst()) {
1712 // Broadcast notification that a new message has
1713 // been allocated
1714 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1715 new Message(messageCursor));
1716 }
1717 } finally {
1718 messageCursor.close();
1719 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001720 }
1721 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001722 }
1723
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001724 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001725 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001726 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001727 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001728 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001729 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001730 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001731 }
1732 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1733 }
1734 }
1735
1736 // Array of the outstanding send or save tasks. Access is synchronized
1737 // with the object itself
1738 /* package for testing */
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001739 @VisibleForTesting
1740 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001741 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001742 private String mSignature;
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07001743 private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001744
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001745 @VisibleForTesting
1746 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07001747 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001748 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001749 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001750 @VisibleForTesting
1751 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001752 final int mRequestId;
1753
Mindy Pereira92551d02012-04-05 11:31:12 -07001754 public SendOrSaveMessage(ReplyFromAccount account, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001755 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001756 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001757 mValues = values;
1758 mRefMessageId = refMessageId;
1759 mSave = save;
1760 mRequestId = mValues.hashCode() ^ hashCode();
1761 }
1762
1763 int requestId() {
1764 return mRequestId;
1765 }
1766 }
1767
1768 /**
1769 * Get the to recipients.
1770 */
1771 public String[] getToAddresses() {
1772 return getAddressesFromList(mTo);
1773 }
1774
1775 /**
1776 * Get the cc recipients.
1777 */
1778 public String[] getCcAddresses() {
1779 return getAddressesFromList(mCc);
1780 }
1781
1782 /**
1783 * Get the bcc recipients.
1784 */
1785 public String[] getBccAddresses() {
1786 return getAddressesFromList(mBcc);
1787 }
1788
1789 public String[] getAddressesFromList(RecipientEditTextView list) {
1790 if (list == null) {
1791 return new String[0];
1792 }
1793 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1794 int count = tokens.length;
1795 String[] result = new String[count];
1796 for (int i = 0; i < count; i++) {
1797 result[i] = tokens[i].toString();
1798 }
1799 return result;
1800 }
1801
1802 /**
1803 * Check for invalid email addresses.
1804 * @param to String array of email addresses to check.
1805 * @param wrongEmailsOut Emails addresses that were invalid.
1806 */
1807 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07001808 if (mValidator == null) {
1809 return;
1810 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001811 for (String email : to) {
1812 if (!mValidator.isValid(email)) {
1813 wrongEmailsOut.add(email);
1814 }
1815 }
1816 }
1817
1818 /**
1819 * Show an error because the user has entered an invalid recipient.
1820 * @param message
1821 */
1822 public void showRecipientErrorDialog(String message) {
1823 // Only 1 invalid recipients error dialog should be allowed up at a
1824 // time.
1825 if (mRecipientErrorDialog != null) {
1826 mRecipientErrorDialog.dismiss();
1827 }
1828 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1829 R.string.recipient_error_dialog_title)
1830 .setIconAttribute(android.R.attr.alertDialogIcon)
Mindy Pereira82cc5662012-01-09 17:29:30 -08001831 .setPositiveButton(
1832 R.string.ok, new Dialog.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001833 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08001834 public void onClick(DialogInterface dialog, int which) {
1835 // after the user dismisses the recipient error
1836 // dialog we want to make sure to refocus the
1837 // recipient to field so they can fix the issue
1838 // easily
1839 if (mTo != null) {
1840 mTo.requestFocus();
1841 }
1842 mRecipientErrorDialog = null;
1843 }
1844 }).show();
1845 }
1846
1847 /**
1848 * Update the state of the UI based on whether or not the current draft
1849 * needs to be saved and the message is not empty.
1850 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001851 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001852 if (mSave != null) {
1853 mSave.setEnabled((shouldSave() && !isBlank()));
1854 }
1855 }
1856
1857 /**
1858 * Returns true if we need to save the current draft.
1859 */
1860 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001861 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001862 // The message should only be saved if:
1863 // It hasn't been sent AND
1864 // Some text has been added to the message OR
1865 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07001866 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07001867 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07001868 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001869 }
1870 }
1871
1872 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001873 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001874 * @return boolean
1875 */
1876 public boolean isBlank() {
1877 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001878 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1879 mBodyView.getText().toString()) == 0)
1880 && mTo.length() == 0
1881 && mCc.length() == 0 && mBcc.length() == 0
1882 && mAttachmentsView.getAttachments().size() == 0;
1883 }
1884
1885 @VisibleForTesting
1886 protected int getSignatureStartPosition(String signature, String bodyText) {
1887 int startPos = -1;
1888
1889 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1890 return startPos;
1891 }
1892
1893 int bodyLength = bodyText.length();
1894 int signatureLength = signature.length();
1895 String printableVersion = convertToPrintableSignature(signature);
1896 int printableLength = printableVersion.length();
1897
1898 if (bodyLength >= printableLength
1899 && bodyText.substring(bodyLength - printableLength)
1900 .equals(printableVersion)) {
1901 startPos = bodyLength - printableLength;
1902 } else if (bodyLength >= signatureLength
1903 && bodyText.substring(bodyLength - signatureLength)
1904 .equals(signature)) {
1905 startPos = bodyLength - signatureLength;
1906 }
1907 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001908 }
1909
1910 /**
1911 * Allows any changes made by the user to be ignored. Called when the user
1912 * decides to discard a draft.
1913 */
1914 private void discardChanges() {
1915 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001916 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001917 mReplyFromChanged = false;
1918 }
1919
1920 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001921 * @param body
1922 * @param save
1923 * @param showToast
1924 * @return Whether the send or save succeeded.
1925 */
1926 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1927 final boolean orientationChanged) {
1928 String[] to, cc, bcc;
1929 Editable body = mBodyView.getEditableText();
Mindy Pereira181df782012-03-01 13:32:44 -08001930 if (orientationChanged) {
1931 to = cc = bcc = new String[0];
1932 } else {
1933 to = getToAddresses();
1934 cc = getCcAddresses();
1935 bcc = getBccAddresses();
1936 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001937
Mindy Pereira181df782012-03-01 13:32:44 -08001938 // Don't let the user send to nobody (but it's okay to save a message
1939 // with no recipients)
1940 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1941 showRecipientErrorDialog(getString(R.string.recipient_needed));
1942 return false;
1943 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001944
Mindy Pereira181df782012-03-01 13:32:44 -08001945 List<String> wrongEmails = new ArrayList<String>();
1946 if (!save) {
1947 checkInvalidEmails(to, wrongEmails);
1948 checkInvalidEmails(cc, wrongEmails);
1949 checkInvalidEmails(bcc, wrongEmails);
1950 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001951
Mindy Pereira181df782012-03-01 13:32:44 -08001952 // Don't let the user send an email with invalid recipients
1953 if (wrongEmails.size() > 0) {
1954 String errorText = String.format(getString(R.string.invalid_recipient),
1955 wrongEmails.get(0));
1956 showRecipientErrorDialog(errorText);
1957 return false;
1958 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001959
Mindy Pereira181df782012-03-01 13:32:44 -08001960 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001961 @Override
Mindy Pereira181df782012-03-01 13:32:44 -08001962 public void onClick(DialogInterface dialog, int which) {
1963 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1964 }
1965 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001966
Mindy Pereira181df782012-03-01 13:32:44 -08001967 // Show a warning before sending only if there are no attachments.
1968 if (!save) {
1969 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1970 boolean warnAboutEmptySubject = isSubjectEmpty();
1971 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001972
Mindy Pereira181df782012-03-01 13:32:44 -08001973 // A warning about an empty body may not be warranted when
1974 // forwarding mails, since a common use case is to forward
1975 // quoted text and not append any more text.
1976 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001977
Mindy Pereira181df782012-03-01 13:32:44 -08001978 // When we bring up a dialog warning the user about a send,
1979 // assume that they accept sending the message. If they do not,
1980 // the dialog listener is required to enable sending again.
1981 if (warnAboutEmptySubject) {
1982 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1983 return true;
1984 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001985
Mindy Pereira181df782012-03-01 13:32:44 -08001986 if (warnAboutEmptyBody) {
1987 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1988 return true;
1989 }
1990 }
1991 // Ask for confirmation to send (if always required)
1992 if (showSendConfirmation()) {
1993 showSendConfirmDialog(R.string.confirm_send_message, listener);
1994 return true;
1995 }
1996 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001997
Mindy Pereira181df782012-03-01 13:32:44 -08001998 sendOrSave(body, save, showToast, false);
1999 return true;
2000 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002001
Mindy Pereira181df782012-03-01 13:32:44 -08002002 /**
2003 * Returns a boolean indicating whether warnings should be shown for empty
2004 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002005 *
Mindy Pereira181df782012-03-01 13:32:44 -08002006 * @return True if a warning should be shown for empty text fields
2007 */
2008 protected boolean showEmptyTextWarnings() {
2009 return mAttachmentsView.getAttachments().size() == 0;
2010 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002011
Mindy Pereira181df782012-03-01 13:32:44 -08002012 /**
2013 * Returns a boolean indicating whether the user should confirm each send
2014 *
2015 * @return True if a warning should be on each send
2016 */
2017 protected boolean showSendConfirmation() {
2018 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2019 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002020
Mindy Pereira181df782012-03-01 13:32:44 -08002021 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
2022 if (mSendConfirmDialog != null) {
2023 mSendConfirmDialog.dismiss();
2024 mSendConfirmDialog = null;
2025 }
2026 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
2027 .setTitle(R.string.confirm_send_title)
2028 .setIconAttribute(android.R.attr.alertDialogIcon)
2029 .setPositiveButton(R.string.send, listener)
Mindy Pereira6edd5972012-06-19 10:22:36 -07002030 .setNegativeButton(R.string.cancel, this)
2031 .show();
Mindy Pereira181df782012-03-01 13:32:44 -08002032 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002033
Mindy Pereira181df782012-03-01 13:32:44 -08002034 /**
2035 * Returns whether the ComposeArea believes there is any text in the body of
2036 * the composition. TODO: When ComposeArea controls the Body as well, add
2037 * that here.
2038 */
2039 public boolean isBodyEmpty() {
2040 return !mQuotedTextView.isTextIncluded();
2041 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002042
Mindy Pereira181df782012-03-01 13:32:44 -08002043 /**
2044 * Test to see if the subject is empty.
2045 *
2046 * @return boolean.
2047 */
2048 // TODO: this will likely go away when composeArea.focus() is implemented
2049 // after all the widget control is moved over.
2050 public boolean isSubjectEmpty() {
2051 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2052 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002053
Mindy Pereira181df782012-03-01 13:32:44 -08002054 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002055 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07002056 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
2057 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002058 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002059
Mindy Pereirac2031972012-04-03 09:38:35 -07002060 String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
2061
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002062 MessageModification.putToAddresses(values, message.getToAddresses());
2063 MessageModification.putCcAddresses(values, message.getCcAddresses());
2064 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002065
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002066 MessageModification.putCustomFromAddress(values, message.from);
Mindy Pereira92551d02012-04-05 11:31:12 -07002067
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002068 MessageModification.putSubject(values, message.subject);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002069 String htmlBody = Html.toHtml(body);
Paul Westbrook05b92b82012-04-20 13:29:37 -07002070
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002071 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2072 StringBuilder fullBody = new StringBuilder(htmlBody);
2073 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002074 // HTML gets converted to text for now
2075 final String text = quotedText.toString();
2076 if (QuotedTextView.containsQuotedText(text)) {
2077 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07002078 final int quoteStartPos = fullBody.length() + pos;
2079 fullBody.append(text);
2080 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07002081 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07002082 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002083 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002084 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2085 // This shouldn't happen, but just use what we have,
2086 // and don't do server-side expansion
2087 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002088 }
2089 }
Mindy Pereira002ff522012-05-30 10:31:26 -07002090 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07002091 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002092 if (refMessage != null) {
2093 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2094 MessageModification.putBodyHtml(values, fullBody.toString());
2095 }
2096 if (!TextUtils.isEmpty(refMessage.bodyText)) {
2097 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
2098 }
2099 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002100 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002101 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
2102 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002103 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002104 if (!TextUtils.isEmpty(refMessageId)) {
2105 MessageModification.putRefMessageId(values, refMessageId);
2106 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002107
Mindy Pereira92551d02012-04-05 11:31:12 -07002108 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(replyFromAccount,
Mindy Pereira181df782012-03-01 13:32:44 -08002109 values, refMessageId, save);
2110 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002111
Mindy Pereira181df782012-03-01 13:32:44 -08002112 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002113
Mindy Pereira181df782012-03-01 13:32:44 -08002114 // Do the send/save action on the specified handler to avoid possible
2115 // ANRs
2116 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002117
Mindy Pereira181df782012-03-01 13:32:44 -08002118 return sendOrSaveMessage.requestId();
2119 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002120
Mindy Pereira002ff522012-05-30 10:31:26 -07002121 private static int getDraftType(int mode) {
2122 int draftType = -1;
2123 switch (mode) {
2124 case ComposeActivity.COMPOSE:
2125 draftType = DraftType.COMPOSE;
2126 break;
2127 case ComposeActivity.REPLY:
2128 draftType = DraftType.REPLY;
2129 break;
2130 case ComposeActivity.REPLY_ALL:
2131 draftType = DraftType.REPLY_ALL;
2132 break;
2133 case ComposeActivity.FORWARD:
2134 draftType = DraftType.FORWARD;
2135 break;
2136 }
2137 return draftType;
2138 }
2139
Mindy Pereira181df782012-03-01 13:32:44 -08002140 private void sendOrSave(Spanned body, boolean save, boolean showToast,
2141 boolean orientationChanged) {
2142 // Check if user is a monkey. Monkeys can compose and hit send
2143 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002144 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002145 return;
2146 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002147
Mindy Pereira181df782012-03-01 13:32:44 -08002148 String[] to, cc, bcc;
2149 if (orientationChanged) {
2150 to = cc = bcc = new String[0];
2151 } else {
2152 to = getToAddresses();
2153 cc = getCcAddresses();
2154 bcc = getBccAddresses();
2155 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002156
Mindy Pereira181df782012-03-01 13:32:44 -08002157 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002158 private int mRestoredRequestId;
2159
Marc Blank0bbc8582012-04-23 15:07:57 -07002160 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002161 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002162 synchronized (mActiveTasks) {
2163 int numTasks = mActiveTasks.size();
2164 if (numTasks == 0) {
2165 // Start service so we won't be killed if this app is
2166 // put in the background.
2167 startService(new Intent(ComposeActivity.this, EmptyService.class));
2168 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002169
Mindy Pereira181df782012-03-01 13:32:44 -08002170 mActiveTasks.add(sendOrSaveTask);
2171 }
2172 if (sTestSendOrSaveCallback != null) {
2173 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2174 }
2175 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002176
Marc Blank0bbc8582012-04-23 15:07:57 -07002177 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002178 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2179 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002180 synchronized (mDraftLock) {
2181 mDraftId = message.id;
2182 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002183 if (sRequestMessageIdMap != null) {
2184 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2185 }
Mindy Pereira181df782012-03-01 13:32:44 -08002186 // Cache request message map, in case the process is killed
2187 saveRequestMap();
2188 }
2189 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002190 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002191 }
2192 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002193
Marc Blank0bbc8582012-04-23 15:07:57 -07002194 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002195 public Message getMessage() {
2196 synchronized (mDraftLock) {
2197 return mDraft;
2198 }
2199 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002200
Marc Blank0bbc8582012-04-23 15:07:57 -07002201 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002202 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07002203 // Update the last sent from account.
2204 if (mAccount != null) {
2205 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2206 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002207 if (success) {
2208 // Successfully sent or saved so reset change markers
2209 discardChanges();
2210 } else {
2211 // A failure happened with saving/sending the draft
2212 // TODO(pwestbro): add a better string that should be used
2213 // when failing to send or save
2214 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2215 .show();
2216 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002217
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002218 int numTasks;
2219 synchronized (mActiveTasks) {
2220 // Remove the task from the list of active tasks
2221 mActiveTasks.remove(task);
2222 numTasks = mActiveTasks.size();
2223 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002224
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002225 if (numTasks == 0) {
2226 // Stop service so we can be killed.
2227 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2228 }
2229 if (sTestSendOrSaveCallback != null) {
2230 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2231 }
2232 }
Mindy Pereira181df782012-03-01 13:32:44 -08002233 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002234
Mindy Pereira181df782012-03-01 13:32:44 -08002235 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07002236 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002237 String fromAddress = selectedAccount.name;
2238 if (selectedAccount == null || fromAddress == null) {
2239 // We don't have either the selected account or from address,
2240 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07002241 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002242 fromAddress = mAccount.name;
2243 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002244
Mindy Pereira181df782012-03-01 13:32:44 -08002245 if (mSendSaveTaskHandler == null) {
2246 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2247 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002248
Mindy Pereira181df782012-03-01 13:32:44 -08002249 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2250 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002251
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002252 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002253 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2254 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Mindy Pereira12575862012-03-21 16:30:54 -07002255 mSendSaveTaskHandler, save, mComposeMode);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002256
Mindy Pereira181df782012-03-01 13:32:44 -08002257 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
2258 mRecipient = selectedAccount.name;
2259 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002260 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002261
Mindy Pereira181df782012-03-01 13:32:44 -08002262 // Don't display the toast if the user is just changing the orientation,
2263 // but we still need to save the draft to the cursor because this is how we restore
2264 // the attachments when the configuration change completes.
2265 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2266 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2267 Toast.LENGTH_LONG).show();
2268 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002269
Mindy Pereira181df782012-03-01 13:32:44 -08002270 // Need to update variables here because the send or save completes
2271 // asynchronously even though the toast shows right away.
2272 discardChanges();
2273 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002274
Mindy Pereira181df782012-03-01 13:32:44 -08002275 // If we are sending, finish the activity
2276 if (!save) {
2277 finish();
2278 }
2279 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002280
Mindy Pereira181df782012-03-01 13:32:44 -08002281 /**
2282 * Save the state of the request messageid map. This allows for the Gmail
2283 * process to be killed, but and still allow for ComposeActivity instances
2284 * to be recreated correctly.
2285 */
2286 private void saveRequestMap() {
2287 // TODO: store the request map in user preferences.
2288 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002289
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07002290 public void openAttachmentTypeSelectionDialog() {
2291 AlertDialog.Builder builder = new AlertDialog.Builder(this);
2292 builder.setTitle(R.string.add_file_attachment);
2293 builder.setAdapter(new AttachmentTypeSelectorAdapter(this),
2294 new DialogInterface.OnClickListener() {
2295 public void onClick(DialogInterface dialog, int position) {
2296 doAttach(position);
2297 }
2298 });
2299 builder.show();
2300 }
2301
2302 private void doAttach(int position) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002303 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2304 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002305 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Andrew Sapperstein8f1c01e2012-06-18 18:15:30 -07002306 i.setType(AttachmentTypeSelectorAdapter.ITEMS.get(position).mMimeType);
Mindy Pereira013194c2012-01-06 15:09:33 -08002307 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002308 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2309 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002310 }
2311
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002312 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002313 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002314 if (mCcBccButton != null) {
2315 mCcBccButton.setVisibility(View.GONE);
2316 }
2317 }
2318
Mindy Pereira326c6602012-01-04 15:32:42 -08002319 @Override
2320 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002321 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002322 if (position == ComposeActivity.REPLY) {
2323 mComposeMode = ComposeActivity.REPLY;
2324 } else if (position == ComposeActivity.REPLY_ALL) {
2325 mComposeMode = ComposeActivity.REPLY_ALL;
2326 } else if (position == ComposeActivity.FORWARD) {
2327 mComposeMode = ComposeActivity.FORWARD;
2328 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002329 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002330 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002331 resetMessageForModeChange();
Mindy Pereiraef388302012-06-18 19:07:44 -07002332 if (mDraft == null && mRefMessage != null) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07002333 setFieldsFromRefMessage(mComposeMode, mAccount.name);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002334 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002335 boolean showCc = false;
2336 boolean showBcc = false;
2337 if (mDraft != null) {
2338 // Following desktop behavior, if the user has added a BCC
2339 // field to a draft, we show it regardless of compose mode.
2340 showBcc = !TextUtils.isEmpty(mDraft.bcc);
2341 // Use the draft to determine what to populate.
2342 // If the Bcc field is showing, show the Cc field whether it is populated or not.
2343 showCc = showBcc || (!TextUtils.isEmpty(mDraft.cc) && mComposeMode == REPLY_ALL);
2344 } else if (mRefMessage != null) {
2345 showCc = mComposeMode == REPLY_ALL && !TextUtils.isEmpty(mRefMessage.cc);
2346 }
2347 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002348 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002349 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002350 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08002351 return true;
2352 }
2353
Mindy Pereirab3112a22012-06-20 12:10:03 -07002354 @VisibleForTesting
2355 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08002356 // When switching between reply, reply all, forward,
2357 // follow the behavior of webview.
2358 // The contents of the following fields are cleared
2359 // so that they can be populated directly from the
2360 // ref message:
2361 // 1) Any recipient fields
2362 // 2) The subject
2363 mTo.setText("");
2364 mCc.setText("");
2365 mBcc.setText("");
2366 // Any edits to the subject are replaced with the original subject.
2367 mSubject.setText("");
2368
2369 // Any changes to the contents of the following fields are kept:
2370 // 1) Body
2371 // 2) Attachments
2372 // If the user made changes to attachments, keep their changes.
2373 if (!mAttachmentsChanged) {
2374 mAttachmentsView.deleteAllAttachments();
2375 }
2376 }
2377
Mindy Pereira326c6602012-01-04 15:32:42 -08002378 private class ComposeModeAdapter extends ArrayAdapter<String> {
2379
2380 private LayoutInflater mInflater;
2381
2382 public ComposeModeAdapter(Context context) {
2383 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2384 .getStringArray(R.array.compose_modes));
2385 }
2386
2387 private LayoutInflater getInflater() {
2388 if (mInflater == null) {
2389 mInflater = LayoutInflater.from(getContext());
2390 }
2391 return mInflater;
2392 }
2393
2394 @Override
2395 public View getView(int position, View convertView, ViewGroup parent) {
2396 if (convertView == null) {
2397 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2398 }
2399 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2400 return super.getView(position, convertView, parent);
2401 }
2402 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002403
2404 @Override
2405 public void onRespondInline(String text) {
2406 appendToBody(text, false);
2407 }
2408
2409 /**
2410 * Append text to the body of the message. If there is no existing body
2411 * text, just sets the body to text.
2412 *
2413 * @param text
2414 * @param withSignature True to append a signature.
2415 */
2416 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002417 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002418 if (bodyText != null && bodyText.length() > 0) {
2419 bodyText.append(text);
2420 } else {
2421 setBody(text, withSignature);
2422 }
2423 }
2424
2425 /**
2426 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002427 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002428 * @param text
2429 * @param withSignature True to append a signature.
2430 */
2431 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002432 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002433 if (withSignature) {
2434 appendSignature();
2435 }
2436 }
2437
2438 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002439 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002440 boolean hasFocus = mBodyView.hasFocus();
Mindy Pereirab13917c2012-03-29 08:08:19 -07002441 if (!TextUtils.equals(newSignature, mSignature)) {
2442 mSignature = newSignature;
2443 if (!TextUtils.isEmpty(mSignature)
2444 && getSignatureStartPosition(mSignature,
2445 mBodyView.getText().toString()) < 0) {
2446 // Appending a signature does not count as changing text.
2447 mBodyView.removeTextChangedListener(this);
2448 mBodyView.append(convertToPrintableSignature(mSignature));
2449 mBodyView.addTextChangedListener(this);
2450 }
Mindy Pereira433b1982012-04-03 11:53:07 -07002451 if (hasFocus) {
2452 focusBody();
2453 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002454 }
2455 }
2456
2457 private String convertToPrintableSignature(String signature) {
2458 String signatureResource = getResources().getString(R.string.signature);
2459 if (signature == null) {
2460 signature = "";
2461 }
2462 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002463 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002464
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08002465 @Override
2466 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07002467 mReplyFromAccount = mFromSpinner.getCurrentAccount();
2468 if (!mAccount.equals(mReplyFromAccount.account)) {
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002469 setAccount(mReplyFromAccount.account);
2470
Mindy Pereira181df782012-03-01 13:32:44 -08002471 // TODO: handle discarding attachments when switching accounts.
2472 // Only enable save for this draft if there is any other content
2473 // in the message.
2474 if (!isBlank()) {
2475 enableSave(true);
2476 }
2477 mReplyFromChanged = true;
2478 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002479 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002480 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002481
2482 public void enableSave(boolean enabled) {
2483 if (mSave != null) {
2484 mSave.setEnabled(enabled);
2485 }
2486 }
2487
2488 public void enableSend(boolean enabled) {
2489 if (mSend != null) {
2490 mSend.setEnabled(enabled);
2491 }
2492 }
2493
2494 /**
2495 * Handles button clicks from any error dialogs dealing with sending
2496 * a message.
2497 */
2498 @Override
2499 public void onClick(DialogInterface dialog, int which) {
2500 switch (which) {
2501 case DialogInterface.BUTTON_POSITIVE: {
2502 doDiscardWithoutConfirmation(true /* show toast */ );
2503 break;
2504 }
2505 case DialogInterface.BUTTON_NEGATIVE: {
2506 // If the user cancels the send, re-enable the send button.
2507 enableSend(true);
2508 break;
2509 }
2510 }
2511
2512 }
2513
Mindy Pereiraefe3d252012-03-01 14:20:44 -08002514 private void doDiscard() {
2515 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2516 .setPositiveButton(R.string.ok, this)
2517 .setNegativeButton(R.string.cancel, null)
2518 .create().show();
2519 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002520 /**
2521 * Effectively discard the current message.
2522 *
2523 * This method is either invoked from the menu or from the dialog
2524 * once the user has confirmed that they want to discard the message.
2525 * @param showToast show "Message discarded" toast if true
2526 */
2527 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002528 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002529 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2530 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07002531 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07002532 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002533 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2534 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07002535 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002536 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002537 // This is not strictly necessary (since we should not try to
2538 // save the draft after calling this) but it ensures that if we
2539 // do save again for some reason we make a new draft rather than
2540 // trying to resave an expunged draft.
2541 mDraftId = UIProvider.INVALID_MESSAGE_ID;
2542 }
2543 }
2544
2545 if (showToast) {
2546 // Display a toast to let the user know
2547 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2548 }
2549
2550 // This prevents the draft from being saved in onPause().
2551 discardChanges();
2552 finish();
2553 }
2554
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002555 private void saveIfNeeded() {
2556 if (mAccount == null) {
2557 // We have not chosen an account yet so there's no way that we can save. This is ok,
2558 // though, since we are saving our state before AccountsActivity is activated. Thus, the
2559 // user has not interacted with us yet and there is no real state to save.
2560 return;
2561 }
2562
2563 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07002564 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002565 }
2566 }
2567
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002568 @Override
2569 public void onAttachmentDeleted() {
2570 mAttachmentsChanged = true;
2571 updateSaveUi();
2572 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002573
2574
2575 /**
2576 * This is called any time one of our text fields changes.
2577 */
Marc Blank0bbc8582012-04-23 15:07:57 -07002578 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002579 public void afterTextChanged(Editable s) {
2580 mTextChanged = true;
2581 updateSaveUi();
2582 }
2583
2584 @Override
2585 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2586 // Do nothing.
2587 }
2588
Marc Blank0bbc8582012-04-23 15:07:57 -07002589 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002590 public void onTextChanged(CharSequence s, int start, int before, int count) {
2591 // Do nothing.
2592 }
2593
2594
2595 // There is a big difference between the text associated with an address changing
2596 // to add the display name or to format properly and a recipient being added or deleted.
2597 // Make sure we only notify of changes when a recipient has been added or deleted.
2598 private class RecipientTextWatcher implements TextWatcher {
2599 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2600
2601 private RecipientEditTextView mView;
2602
2603 private TextWatcher mListener;
2604
2605 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2606 mView = view;
2607 mListener = listener;
2608 }
2609
2610 @Override
2611 public void afterTextChanged(Editable s) {
2612 if (hasChanged()) {
2613 mListener.afterTextChanged(s);
2614 }
2615 }
2616
2617 private boolean hasChanged() {
2618 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2619 int totalCount = currRecips.length;
2620 int totalPrevCount = 0;
2621 for (Entry<String, Integer> entry : mContent.entrySet()) {
2622 totalPrevCount += entry.getValue();
2623 }
2624 if (totalCount != totalPrevCount) {
2625 return true;
2626 }
2627
2628 for (String recip : currRecips) {
2629 if (!mContent.containsKey(recip)) {
2630 return true;
2631 } else {
2632 int count = mContent.get(recip) - 1;
2633 if (count < 0) {
2634 return true;
2635 } else {
2636 mContent.put(recip, count);
2637 }
2638 }
2639 }
2640 return false;
2641 }
2642
2643 private String[] tokenizeRecips(String[] recips) {
2644 // Tokenize them all and put them in the list.
2645 String[] recipAddresses = new String[recips.length];
2646 for (int i = 0; i < recips.length; i++) {
2647 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2648 }
2649 return recipAddresses;
2650 }
2651
2652 @Override
2653 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2654 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2655 for (String recip : recips) {
2656 if (!mContent.containsKey(recip)) {
2657 mContent.put(recip, 1);
2658 } else {
2659 mContent.put(recip, (mContent.get(recip)) + 1);
2660 }
2661 }
2662 }
2663
2664 @Override
2665 public void onTextChanged(CharSequence s, int start, int before, int count) {
2666 // Do nothing.
2667 }
2668 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002669
2670 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
2671 if (sTestSendOrSaveCallback != null && testCallback != null) {
2672 throw new IllegalStateException("Attempting to register more than one test callback");
2673 }
2674 sTestSendOrSaveCallback = testCallback;
2675 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07002676
2677 @VisibleForTesting
2678 protected ArrayList<Attachment> getAttachments() {
2679 return mAttachmentsView.getAttachments();
2680 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07002681
2682 @Override
2683 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
2684 switch (id) {
2685 case REFERENCE_MESSAGE_LOADER:
2686 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
2687 null, null);
2688 }
2689 return null;
2690 }
2691
2692 @Override
2693 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
2694 if (data != null && data.moveToFirst()) {
2695 mRefMessage = new Message(data);
2696 // We set these based on EXTRA_TO.
2697 mRefMessage.to = null;
2698 mRefMessage.from = null;
2699 Intent intent = getIntent();
2700 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
2701 initFromRefMessage(action, mAccount.name);
2702 finishSetup(action, intent, null, true);
2703 if (action != FORWARD) {
2704 String to = intent.getStringExtra(EXTRA_TO);
2705 if (!TextUtils.isEmpty(to)) {
2706 clearChangeListeners();
2707 mTo.append(to);
2708 initChangeListeners();
2709 }
2710 }
2711 } else {
2712 finish();
2713 }
2714 }
2715
2716 @Override
2717 public void onLoaderReset(Loader<Cursor> arg0) {
2718 // Do nothing.
2719 }
2720}