blob: e38243f65613e4d7db3643a9710a62a3721ef293 [file] [log] [blame]
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001/**
2 * Copyright (c) 2011, Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Mindy Pereira326c6602012-01-04 15:32:42 -080019import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070020import android.app.ActionBar.OnNavigationListener;
21import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080022import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.Dialog;
Mindy Pereira6349a042012-01-04 11:25:01 -080025import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080026import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080027import android.content.Context;
Mindy Pereira82cc5662012-01-09 17:29:30 -080028import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080029import android.content.Intent;
Mindy Pereira82cc5662012-01-09 17:29:30 -080030import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080031import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080032import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080033import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080034import android.os.Handler;
35import android.os.HandlerThread;
Paul Westbrookf97588b2012-03-20 11:11:37 -070036import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080037import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080038import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.text.Html;
40import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080041import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080042import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080043import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080044import android.text.util.Rfc822Tokenizer;
Mindy Pereira326c6602012-01-04 15:32:42 -080045import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080046import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080049import android.view.View;
50import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070051import android.view.ViewGroup;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080052import android.view.inputmethod.BaseInputConnection;
Mindy Pereira326c6602012-01-04 15:32:42 -080053import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080054import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070055import android.widget.EditText;
Mindy Pereira1f936682012-03-02 11:30:33 -080056import android.widget.ImageView;
Mindy Pereira6349a042012-01-04 11:25:01 -080057import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080058import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080059
Mindy Pereirac17d0732011-12-29 10:46:19 -080060import com.android.common.Rfc822Validator;
Andy Huang5c5fd572012-04-08 18:19:29 -070061import com.android.ex.chips.RecipientEditTextView;
62import com.android.mail.R;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -080063import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080064import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080065import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080066import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080067import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080068import com.android.mail.providers.Address;
69import com.android.mail.providers.Attachment;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080070import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080071import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070072import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080073import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080074import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070075import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070076import com.android.mail.providers.UIProvider.DraftType;
Paul Westbrook92227f62012-03-20 10:32:51 -070077import com.android.mail.utils.AccountUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080078import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080079import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080080import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -080081import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080082import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080083
Mindy Pereira62de1b12012-04-06 12:17:56 -070084import org.json.JSONException;
85
Mindy Pereira8eca57a2012-03-20 16:42:34 -070086import java.io.UnsupportedEncodingException;
87import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080088import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070089import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080090import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -080091import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080092import java.util.HashSet;
93import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -070094import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -070095import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -080096import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080097
98public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080099 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700100 AttachmentDeletedListener, OnAccountChangedListener {
Mindy Pereira6349a042012-01-04 11:25:01 -0800101 // Identifiers for which type of composition this is
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700102 static final int COMPOSE = -1;
Mindy Pereira6349a042012-01-04 11:25:01 -0800103 static final int REPLY = 0;
104 static final int REPLY_ALL = 1;
105 static final int FORWARD = 2;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700106 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800107
108 // Integer extra holding one of the above compose action
109 private static final String EXTRA_ACTION = "action";
110
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700111 private static final String EXTRA_SHOW_CC_BCC = "showCcBcc";
112
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700113 private static final String UTF8_ENCODING_NAME = "UTF-8";
114
115 private static final String MAIL_TO = "mailto";
116
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700117 private static final String EXTRA_SUBJECT = "subject";
118
119 private static final String EXTRA_BODY = "body";
120
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700121 private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
122
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700123 // Extra that we can get passed from other activities
124 private static final String EXTRA_TO = "to";
125 private static final String EXTRA_CC = "cc";
126 private static final String EXTRA_BCC = "bcc";
127
128 // List of all the fields
129 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
130
Mindy Pereira82cc5662012-01-09 17:29:30 -0800131 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
132 // Map containing information about requests to create new messages, and the id of the
133 // messages that were the result of those requests.
134 //
135 // This map is used when the activity that initiated the save a of a new message, is killed
136 // before the save has completed (and when we know the id of the newly created message). When
137 // a save is completed, the service that is running in the background, will update the map
138 //
139 // When a new ComposeActivity instance is created, it will attempt to use the information in
140 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
141 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
142 // to populate the map with data stored in shared preferences.
143 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
144 // Key used to store the above map
145 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800146 /**
147 * Notifies the {@code Activity} that the caller is an Email
148 * {@code Activity}, so that the back behavior may be modified accordingly.
149 *
150 * @see #onAppUpPressed
151 */
152 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
153
Paul Westbrookf97588b2012-03-20 11:11:37 -0700154 static final String EXTRA_ATTACHMENTS = "attachments";
155
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800156 // If this is a reply/forward then this extra will hold the original message
157 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700158 // If this is an action to edit an existing draft messagge, this extra will hold the
159 // draft message
160 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800161 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800162 private static final String LOG_TAG = new LogUtils().getLogTag();
163 // Request numbers for activities we start
164 private static final int RESULT_PICK_ATTACHMENT = 1;
165 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700166 // TODO(mindyp) set mime-type for auto send?
167 private static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
168
169 // Max size for attachments (5 megs). Will be overridden by account settings if found.
170 // TODO(mindyp): read this from account settings?
171 private static final int DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700172 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
173 private static final String EXTRA_REQUEST_ID = "requestId";
174 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
175 private static final String EXTRA_FOCUS_SELECTION_END = null;
176 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800177
Mindy Pereira82cc5662012-01-09 17:29:30 -0800178 /**
179 * A single thread for running tasks in the background.
180 */
181 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800182 private RecipientEditTextView mTo;
183 private RecipientEditTextView mCc;
184 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800185 private Button mCcBccButton;
186 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800187 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800188 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700189 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800190 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800191 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800192 private TextView mSubject;
193
Mindy Pereira326c6602012-01-04 15:32:42 -0800194 private ComposeModeAdapter mComposeModeAdapter;
195 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800196 private boolean mForward;
197 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800198 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700199 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800200 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800201 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800202 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800203 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800204 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800205 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800206 private boolean mTextChanged;
207 private boolean mReplyFromChanged;
208 private MenuItem mSave;
209 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800210 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800211 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800212 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800213 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
214 private Message mDraft;
215 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800216 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800217
Mindy Pereira326c6602012-01-04 15:32:42 -0800218 /**
219 * Can be called from a non-UI thread.
220 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800221 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700222 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800223 }
224
Mindy Pereira6349a042012-01-04 11:25:01 -0800225 /**
226 * Can be called from a non-UI thread.
227 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800228 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800229 launch(launcher, account, null, COMPOSE);
230 }
231
232 /**
233 * Can be called from a non-UI thread.
234 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800235 public static void reply(Context launcher, Account account, Message message) {
236 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800237 }
238
239 /**
240 * Can be called from a non-UI thread.
241 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800242 public static void replyAll(Context launcher, Account account, Message message) {
243 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800244 }
245
246 /**
247 * Can be called from a non-UI thread.
248 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800249 public static void forward(Context launcher, Account account, Message message) {
250 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800251 }
252
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800253 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800254 Intent intent = new Intent(launcher, ComposeActivity.class);
255 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
256 intent.putExtra(EXTRA_ACTION, action);
257 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700258 if (action == EDIT_DRAFT) {
259 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
260 } else {
261 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
262 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800263 launcher.startActivity(intent);
264 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800265
266 @Override
267 public void onCreate(Bundle savedInstanceState) {
268 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800269 setContentView(R.layout.compose);
270 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800271 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700272 Account account;
273 Message message;
274 int action;
275 if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE)) {
276 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
277 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
278 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
279 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
280 } else {
281 account = (Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
282 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
283 // Initialize the message from the message in the intent
284 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
285 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
286 }
Paul Westbrook92227f62012-03-20 10:32:51 -0700287 if (account == null) {
288 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
289 if (syncingAccounts.length > 0) {
290 account = syncingAccounts[0];
291 }
292 }
293
294 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800295 if (mAccount == null) {
296 return;
297 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700298
299 if (message != null && action != EDIT_DRAFT) {
300 initFromDraftMessage(message);
301 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700302 showCcBcc(savedInstanceState);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700303 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700304 initFromDraftMessage(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700305 // Update the action to the draft type of the previous draft
306 switch (message.draftType) {
307 case UIProvider.DraftType.REPLY:
308 action = REPLY;
309 break;
310 case UIProvider.DraftType.REPLY_ALL:
311 action = REPLY_ALL;
312 break;
313 case UIProvider.DraftType.FORWARD:
314 action = FORWARD;
315 break;
316 case UIProvider.DraftType.COMPOSE:
317 default:
318 action = COMPOSE;
319 break;
320 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700321 initQuotedTextFromRefMessage(mRefMessage, action);
322 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
323 if (mRefMessage != null) {
324 initFromRefMessage(action, mAccount.name);
325 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700326 } else {
327 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700328 }
329
330 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800331 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800332 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800333 initRecipients();
Paul Westbrookf97588b2012-03-20 11:11:37 -0700334 initAttachmentsFromIntent(intent);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800335 initActionBar(action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700336 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
337 action);
Mindy Pereira75f66632012-01-11 11:42:02 -0800338 initChangeListeners();
Mindy Pereira433b1982012-04-03 11:53:07 -0700339 setFocus(action);
340 }
341
342 private void setFocus(int action) {
343 if (action == EDIT_DRAFT) {
344 int type = mDraft.draftType;
345 switch (type) {
346 case UIProvider.DraftType.COMPOSE:
347 case UIProvider.DraftType.FORWARD:
348 action = COMPOSE;
349 break;
350 case UIProvider.DraftType.REPLY:
351 case UIProvider.DraftType.REPLY_ALL:
352 default:
353 action = REPLY;
354 break;
355 }
356 }
357 switch (action) {
358 case FORWARD:
359 case COMPOSE:
360 mTo.requestFocus();
361 break;
362 case REPLY:
363 case REPLY_ALL:
364 default:
365 focusBody();
366 break;
367 }
368 }
369
370 /**
371 * Focus the body of the message.
372 */
373 public void focusBody() {
374 mBodyView.requestFocus();
375 int length = mBodyView.getText().length();
376
377 int signatureStartPos = getSignatureStartPosition(
378 mSignature, mBodyView.getText().toString());
379 if (signatureStartPos > -1) {
380 // In case the user deleted the newlines...
381 mBodyView.setSelection(signatureStartPos);
382 } else if (length > 0) {
383 // Move cursor to the end.
384 mBodyView.setSelection(length);
385 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800386 }
387
388 @Override
389 protected void onResume() {
390 super.onResume();
391 // Update the from spinner as other accounts
392 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800393 if (mFromSpinner != null && mAccount != null) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700394 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount);
Mindy Pereira818143e2012-01-11 13:59:49 -0800395 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800396 }
397
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800398 @Override
399 protected void onPause() {
400 super.onPause();
401
402 if (mSendConfirmDialog != null) {
403 mSendConfirmDialog.dismiss();
404 }
405 if (mRecipientErrorDialog != null) {
406 mRecipientErrorDialog.dismiss();
407 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800408 saveIfNeeded();
409 }
410
411 @Override
412 protected final void onActivityResult(int request, int result, Intent data) {
413 mAddingAttachment = false;
414
415 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
416 addAttachmentAndUpdateView(data);
417 }
418 }
419
420 @Override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700421 public final void onRestoreInstanceState(Bundle savedInstanceState) {
422 super.onRestoreInstanceState(savedInstanceState);
423 if (savedInstanceState != null) {
424 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
425 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
426 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
427 // There should be a focus and it should be an EditText since we
428 // only save these extras if these conditions are true.
429 EditText focusEditText = (EditText) getCurrentFocus();
430 final int length = focusEditText.getText().length();
431 if (selectionStart < length && selectionEnd < length) {
432 focusEditText.setSelection(selectionStart, selectionEnd);
433 }
434 }
435 }
436 }
437
438 @Override
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800439 public final void onSaveInstanceState(Bundle state) {
440 super.onSaveInstanceState(state);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700441 // The framework is happy to save and restore the selection but only if it also saves and
442 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
443 // this manually.
444 View focus = getCurrentFocus();
445 if (focus != null && focus instanceof EditText) {
446 EditText focusEditText = (EditText) focus;
447 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
448 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
449 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700450
451 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700452 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Paul Westbrook6273e962012-04-23 10:44:15 -0700453 final ReplyFromAccount selectedReplyFromAccount =
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700454 (replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
455 replyFromAccounts.get(selectedPos) :
Paul Westbrook6273e962012-04-23 10:44:15 -0700456 null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700457 if (selectedReplyFromAccount != null) {
458 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
459 .toString());
460 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
461 } else {
462 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
463 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800464
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700465 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
466 // We don't have a draft id, and we have a request id,
467 // save the request id.
468 state.putInt(EXTRA_REQUEST_ID, mRequestId);
469 }
470
471 // We want to restore the current mode after a pause
472 // or rotation.
473 int mode = getMode();
474 state.putInt(EXTRA_ACTION, mode);
475
476 Message message = createMessage(selectedReplyFromAccount, mode);
477 state.putParcelable(EXTRA_MESSAGE, message);
478
479 if (mRefMessage != null) {
480 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
481 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700482 state.putBoolean(EXTRA_SHOW_CC_BCC, mCcBccView.isVisible());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700483 }
484
485 private int getMode() {
486 int mode = ComposeActivity.COMPOSE;
487 ActionBar actionBar = getActionBar();
488 if (actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
489 mode = actionBar.getSelectedNavigationIndex();
490 }
491 return mode;
492 }
493
494 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
495 Message message = new Message();
496 message.id = UIProvider.INVALID_MESSAGE_ID;
497 message.serverId =UIProvider.INVALID_MESSAGE_ID;
498 message.uri = null;
499 message.conversationUri = null;
500 message.subject = mSubject.getText().toString();
501 message.snippet = null;
502 message.from = selectedReplyFromAccount.name;
503 message.to = mTo.getText().toString();
Mindy Pereira4b1377e2012-04-18 15:08:05 -0700504 message.cc = mCc.getText().toString();
505 message.bcc = mBcc.getText().toString();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700506 message.replyTo = null;
507 message.dateReceivedMs = 0;
508 String htmlBody = Html.toHtml(mBodyView.getText());
509 StringBuilder fullBody = new StringBuilder(htmlBody);
510 message.bodyHtml = fullBody.toString();
511 message.bodyText = mBodyView.getText().toString();
512 message.embedsExternalResources = false;
513 message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
514 message.draftType = mode;
515 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
516 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
517 message.hasAttachments = attachments != null && attachments.size() > 0;
518 message.attachmentListUri = null;
519 message.messageFlags = 0;
520 message.saveUri = null;
521 message.sendUri = null;
522 message.alwaysShowImages = false;
523 message.attachmentsJson = Attachment.toJSONArray(attachments);
524 CharSequence quotedText = mQuotedTextView.getQuotedText();
525 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
526 .getQuotedTextOffset(quotedText.toString()) : -1;
527 message.accountUri = null;
528 return message;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800529 }
530
Mindy Pereira818143e2012-01-11 13:59:49 -0800531 @VisibleForTesting
532 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700533 if (account == null) {
534 return;
535 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700536 if (!account.equals(mAccount)) {
537 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700538 mCachedSettings = mAccount.settings;
539 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700540 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800541 }
542
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700543 private void initFromSpinner(Bundle bundle, int action) {
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700544 String accountString = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700545 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700546 action = COMPOSE;
547 }
548 mFromSpinner.asyncInitFromSpinner(action, mAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700549 if (bundle != null) {
550 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
551 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
552 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
553 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
554 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
555 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
556 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700557 }
558 if (mReplyFromAccount == null) {
559 if (mDraft != null) {
560 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
561 } else if (mRefMessage != null) {
562 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
563 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700564 }
565 if (mReplyFromAccount == null) {
566 mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
567 mAccount.name, true, false);
568 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700569
Mindy Pereira62de1b12012-04-06 12:17:56 -0700570 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700571
Mindy Pereira62de1b12012-04-06 12:17:56 -0700572 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700573 // If there is only 1 account, just show that account.
574 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700575 // send mail from / save drafts to.
576 mFromStatic.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700577 mFromStaticText.setText(mAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700578 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700579 } else {
580 mFromStatic.setVisibility(View.VISIBLE);
581 mFromStaticText.setText(mAccount.name);
582 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700583 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800584 }
585
Mindy Pereira62de1b12012-04-06 12:17:56 -0700586 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
587 if (refMessage.accountUri != null) {
588 // This must be from combined inbox.
589 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
590 for (ReplyFromAccount from : replyFromAccounts) {
591 if (from.account.uri.equals(refMessage.accountUri)) {
592 return from;
593 }
594 }
595 return null;
596 } else {
597 return getReplyFromAccount(account, refMessage);
598 }
599 }
600
601 /**
602 * Given an account and which email address the message was sent to,
603 * return who the message should be sent from.
604 * @param account Account in which the message arrived.
605 * @param sentTo Email address to which the message was sent.
606 * @return the address from which to reply.
607 */
608 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
609 // First see if we are supposed to use the default address or
610 // the address it was sentTo.
611 if (false) { //mCachedSettings.forceReplyFromDefault) {
612 return getDefaultReplyFromAccount(account);
613 } else {
614 // If we aren't explicityly told which account to look for, look at
615 // all the message recipients and find one that matches
616 // a custom from or account.
617 List<String> allRecipients = new ArrayList<String>();
618 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.to)));
619 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.cc)));
620 return getMatchingRecipient(account, allRecipients);
621 }
622 }
623
624 /**
625 * Compare all the recipients of an email to the current account and all
626 * custom addresses associated with that account. Return the match if there
627 * is one, or the default account if there isn't.
628 */
629 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
630 // Tokenize the list and place in a hashmap.
631 ReplyFromAccount matchingReplyFrom = null;
632 Rfc822Token[] tokens;
633 HashSet<String> recipientsMap = new HashSet<String>();
634 for (String address : sentTo) {
635 tokens = Rfc822Tokenizer.tokenize(address);
636 for (int i = 0; i < tokens.length; i++) {
637 recipientsMap.add(tokens[i].getAddress());
638 }
639 }
640
641 int matchingAddressCount = 0;
642 List<ReplyFromAccount> customFroms;
643 try {
644 customFroms = FromAddressSpinner.getAccountSpecificFroms(account);
645 if (customFroms != null) {
646 for (ReplyFromAccount entry : customFroms) {
647 if (recipientsMap.contains(entry.address)) {
648 matchingReplyFrom = entry;
649 matchingAddressCount++;
650 }
651 }
652 }
653 } catch (JSONException e) {
654 LogUtils.wtf(LOG_TAG, "Exception parsing from addresses for account %s",
655 account.name);
656 }
657 if (matchingAddressCount > 1) {
658 matchingReplyFrom = getDefaultReplyFromAccount(account);
659 }
660 return matchingReplyFrom;
661 }
662
663 private ReplyFromAccount getDefaultReplyFromAccount(Account account) {
664 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
665 for (ReplyFromAccount from : replyFromAccounts) {
666 if (from.isDefault) {
667 return from;
668 }
669 }
670 return new ReplyFromAccount(account, account.uri, account.name, account.name, true, false);
671 }
672
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700673 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
674 String sender = msg.from;
Mindy Pereira62de1b12012-04-06 12:17:56 -0700675 ReplyFromAccount replyFromAccount = null;
676 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
677 if (TextUtils.equals(account.name, sender)) {
678 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
679 mAccount.name, true, false);
680 } else {
681 for (ReplyFromAccount fromAccount : replyFromAccounts) {
682 if (TextUtils.equals(fromAccount.name, sender)) {
683 replyFromAccount = fromAccount;
684 break;
685 }
686 }
687 }
688 return replyFromAccount;
689 }
690
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800691 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800692 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800693 if (mCcBccButton != null) {
694 mCcBccButton.setOnClickListener(this);
695 }
696 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800697 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800698 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
699 if (mAttachmentsButton != null) {
700 mAttachmentsButton.setOnClickListener(this);
701 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800702 mTo = (RecipientEditTextView) findViewById(R.id.to);
703 mCc = (RecipientEditTextView) findViewById(R.id.cc);
704 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800705 // TODO: add special chips text change watchers before adding
706 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800707 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800708 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
709 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -0700710 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800711 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800712 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800713 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800714 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800715 }
716
Mindy Pereira75f66632012-01-11 11:42:02 -0800717 // Now that the message has been initialized from any existing draft or
718 // ref message data, set up listeners for any changes that occur to the
719 // message.
720 private void initChangeListeners() {
721 mSubject.addTextChangedListener(this);
722 mBodyView.addTextChangedListener(this);
723 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
724 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
725 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
726 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800727 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800728 }
729
Mindy Pereira326c6602012-01-04 15:32:42 -0800730 private void initActionBar(int action) {
731 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800732 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800733 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800734 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
735 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800736 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800737 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800738 if (mComposeModeAdapter == null) {
739 mComposeModeAdapter = new ComposeModeAdapter(this);
740 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800741 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
742 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800743 switch (action) {
744 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800745 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800746 break;
747 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800748 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800749 break;
750 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800751 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800752 break;
753 }
754 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700755 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
756 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
757 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800758 }
759
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800760 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700761 setSubject(mRefMessage, action);
762 // Setup recipients
763 if (action == FORWARD) {
764 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800765 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700766 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700767 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700768 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
769 initAttachments(mRefMessage);
770 }
771 updateHideOrShowCcBcc();
Mindy Pereirac17d0732011-12-29 10:46:19 -0800772 }
773
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700774 private void initFromDraftMessage(Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700775 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
776
777 mDraft = message;
778 mDraftId = message.id;
779 mSubject.setText(message.subject);
780 mForward = message.draftType == UIProvider.DraftType.FORWARD;
781 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
782 addToAddresses(toAddresses);
783 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
784 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -0700785 if (message.hasAttachments) {
786 List<Attachment> attachments = message.getAttachments();
787 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -0700788 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -0700789 }
790 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700791
792 // Set the body
793 if (!TextUtils.isEmpty(message.bodyHtml)) {
794 mBodyView.setText(Html.fromHtml(message.bodyHtml));
795 } else {
796 mBodyView.setText(message.bodyText);
797 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700798 }
799
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700800 /**
801 * Fill all the widgets with the content found in the Intent Extra, if any.
802 * Also apply the same style to all widgets. Note: if initFromExtras is
803 * called as a result of switching between reply, reply all, and forward per
804 * the latest revision of Gmail, and the user has already made changes to
805 * attachments on a previous incarnation of the message (as a reply, reply
806 * all, or forward), the original attachments from the message will not be
807 * re-instantiated. The user's changes will be respected. This follows the
808 * web gmail interaction.
809 */
810 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700811 // If we were invoked with a SENDTO intent, the value
812 // should take precedence
813 final Uri dataUri = intent.getData();
814 if (dataUri != null) {
815 if (MAIL_TO.equals(dataUri.getScheme())) {
816 initFromMailTo(dataUri.toString());
817 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -0700818 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700819 String toText = dataUri.getSchemeSpecificPart();
820 if (toText != null) {
821 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -0700822 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700823 }
824 }
825 }
826 }
827
828 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
829 if (extraStrings != null) {
830 addToAddresses(Arrays.asList(extraStrings));
831 }
832 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
833 if (extraStrings != null) {
834 addCcAddresses(Arrays.asList(extraStrings), null);
835 }
836 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
837 if (extraStrings != null) {
838 addBccAddresses(Arrays.asList(extraStrings));
839 }
840
841 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
842 if (extraString != null) {
843 mSubject.setText(extraString);
844 }
845
846 for (String extra : ALL_EXTRAS) {
847 if (intent.hasExtra(extra)) {
848 String value = intent.getStringExtra(extra);
849 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700850 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700851 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700852 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700853 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700854 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700855 } else if (EXTRA_SUBJECT.equals(extra)) {
856 mSubject.setText(value);
857 } else if (EXTRA_BODY.equals(extra)) {
858 setBody(value, true /* with signature */);
859 }
860 }
861 }
862
863 Bundle extras = intent.getExtras();
864 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700865 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
866 if (text != null) {
867 setBody(text, true /* with signature */);
868 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700869 }
870
871 updateHideOrShowCcBcc();
872 }
873
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700874 private void initFromMessageInIntent(Message message) {
875 mTo.append(message.to);
876 mCc.append(message.cc);
877 mBcc.append(message.bcc);
878 mBodyView.setText(message.bodyText);
879 mSubject.setText(message.subject);
880 List<Attachment> attachments = message.getAttachments();
881 for (Attachment a : attachments) {
882 mAttachmentsView.addAttachment(a);
883 }
884 mQuotedTextView.updateCheckedState(message.appendRefMessageContent);
885 }
886
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700887 @VisibleForTesting
888 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
889 // TODO: handle the case where there are spaces in the display name as well as the email
890 // such as "Guy with spaces <guy+with+spaces@gmail.com>" as they it could be encoded
891 // ambiguously.
892
893 // Since URLDecode.decode changes + into ' ', and + is a valid
894 // email character, we need to find/ replace these ourselves before
895 // decoding.
896 String replacePlus = s.replace("+", "%2B");
897 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
898 }
899
900 /**
901 * Initialize the compose view from a String representing a mailTo uri.
902 * @param mailToString The uri as a string.
903 */
904 public void initFromMailTo(String mailToString) {
905 // We need to disguise this string as a URI in order to parse it
906 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
907 Uri uri = Uri.parse("foo://" + mailToString);
908 int index = mailToString.indexOf("?");
909 int length = "mailto".length() + 1;
910 String to;
911 try {
912 // Extract the recipient after mailto:
913 if (index == -1) {
914 to = decodeEmailInUri(mailToString.substring(length));
915 } else {
916 to = decodeEmailInUri(mailToString.substring(length, index));
917 }
Mindy Pereiradbe89962012-04-13 09:42:38 -0700918 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700919 } catch (UnsupportedEncodingException e) {
920 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
921 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
922 } else {
923 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
924 }
925 }
926
927 List<String> cc = uri.getQueryParameters("cc");
928 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
929
930 List<String> otherTo = uri.getQueryParameters("to");
931 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
932
933 List<String> bcc = uri.getQueryParameters("bcc");
934 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
935
936 List<String> subject = uri.getQueryParameters("subject");
937 if (subject.size() > 0) {
938 try {
939 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
940 } catch (UnsupportedEncodingException e) {
941 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
942 e.getMessage(), subject);
943 }
944 }
945
946 List<String> body = uri.getQueryParameters("body");
947 if (body.size() > 0) {
948 try {
949 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
950 true /* with signature */);
951 } catch (UnsupportedEncodingException e) {
952 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
953 }
954 }
955
956 updateHideOrShowCcBcc();
957 }
958
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800959 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800960 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800961 }
962
Paul Westbrookf97588b2012-03-20 11:11:37 -0700963 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -0700964 Bundle extras = intent.getExtras();
965 if (extras == null) {
966 extras = Bundle.EMPTY;
967 }
Paul Westbrookf97588b2012-03-20 11:11:37 -0700968 final String action = intent.getAction();
969 if (!mAttachmentsChanged) {
970 long totalSize = 0;
971 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
972 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
973 for (String uriString : uris) {
974 final Uri uri = Uri.parse(uriString);
975 long size = 0;
976 try {
Andy Huang5c5fd572012-04-08 18:19:29 -0700977 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -0700978 } catch (AttachmentFailureException e) {
979 // A toast has already been shown to the user,
980 // just break out of the loop.
981 LogUtils.e(LOG_TAG, e, "Error adding attachment");
982 }
983 totalSize += size;
984 }
985 }
986 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
987 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
988 long size = 0;
989 try {
Andy Huang5c5fd572012-04-08 18:19:29 -0700990 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -0700991 } catch (AttachmentFailureException e) {
992 // A toast has already been shown to the user, so just
993 // exit.
994 LogUtils.e(LOG_TAG, e, "Error adding attachment");
995 }
996 totalSize += size;
997 }
998
999 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
1000 && extras.containsKey(Intent.EXTRA_STREAM)) {
1001 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
1002 for (Parcelable uri : uris) {
1003 long size = 0;
1004 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001005 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001006 } catch (AttachmentFailureException e) {
1007 // A toast has already been shown to the user,
1008 // just break out of the loop.
1009 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1010 }
1011 totalSize += size;
1012 }
1013 }
1014
1015 if (totalSize > 0) {
1016 mAttachmentsChanged = true;
1017 updateSaveUi();
1018 }
1019 }
1020 }
1021
1022
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001023 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1024 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001025 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1026 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001027 }
1028
1029 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001030 // Its possible there is a menu item OR a button.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001031 boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
1032 boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
1033 if (ccVisible || bccVisible) {
1034 mCcBccView.show(false, ccVisible, bccVisible);
1035 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001036 if (mCcBccButton != null) {
1037 if (!mCc.isShown() || !mBcc.isShown()) {
1038 mCcBccButton.setVisibility(View.VISIBLE);
1039 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
1040 : R.string.add_bcc_label));
1041 } else {
1042 mCcBccButton.setVisibility(View.GONE);
1043 }
1044 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001045 }
1046
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001047 private void showCcBcc(Bundle state) {
1048 if (state != null && state.containsKey(EXTRA_SHOW_CC_BCC)) {
1049 boolean show = state.getBoolean(EXTRA_SHOW_CC_BCC);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001050 if (show) {
1051 mCcBccView.show(false, show, show);
1052 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001053 }
1054 }
1055
Mindy Pereira013194c2012-01-06 15:09:33 -08001056 /**
1057 * Add attachment and update the compose area appropriately.
1058 * @param data
1059 */
1060 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001061 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1062 }
1063
Andy Huang5c5fd572012-04-08 18:19:29 -07001064 public void addAttachmentAndUpdateView(Uri contentUri) {
1065 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001066 return;
1067 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001068 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001069 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1070 } catch (AttachmentFailureException e) {
1071 // A toast has already been shown to the user, no need to do
1072 // anything.
1073 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1074 }
1075 }
1076
1077 public void addAttachmentAndUpdateView(Attachment attachment) {
1078 try {
1079 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001080 if (size > 0) {
1081 mAttachmentsChanged = true;
1082 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001083 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001084 } catch (AttachmentFailureException e) {
1085 // A toast has already been shown to the user, no need to do
1086 // anything.
1087 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -08001088 }
1089 }
1090
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001091 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001092 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001093 // Don't populate the address if this is a forward.
1094 if (action == ComposeActivity.FORWARD) {
1095 return;
1096 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001097 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001098 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001099
Mindy Pereira818143e2012-01-11 13:59:49 -08001100 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001101 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001102 // This is the email address of the current user, i.e. the one composing
1103 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001104 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001105 String fromAddress = refMessage.from;
1106 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
1107 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001108 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001109
1110 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1111 // Cc list is the union of the To and Cc recipients of the original
1112 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001113 // already on the To list.
1114 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001115 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1116 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001117 addToAddresses(toAddresses);
1118 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001119 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001120 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1121 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -08001122 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001123 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001124 addRecipients(accountEmail, ccAddresses,
1125 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001126 addCcAddresses(ccAddresses, toAddresses);
1127 }
1128 }
1129
1130 private void addToAddresses(Collection<String> addresses) {
1131 addAddressesToList(addresses, mTo);
1132 }
1133
1134 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001135 addCcAddressesToList(tokenizeAddressList(addresses),
1136 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001137 }
1138
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001139 private void addBccAddresses(Collection<String> addresses) {
1140 addAddressesToList(addresses, mBcc);
1141 }
1142
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001143 @VisibleForTesting
1144 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1145 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1146 String address;
1147
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001148 if (compareToList == null) {
1149 for (Rfc822Token[] tokens : addresses) {
1150 for (int i = 0; i < tokens.length; i++) {
1151 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001152 list.append(address + END_TOKEN);
1153 }
1154 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001155 } else {
1156 HashSet<String> compareTo = convertToHashSet(compareToList);
1157 for (Rfc822Token[] tokens : addresses) {
1158 for (int i = 0; i < tokens.length; i++) {
1159 address = tokens[i].toString();
1160 // Check if this is a duplicate:
1161 if (!compareTo.contains(tokens[i].getAddress())) {
1162 // Get the address here
1163 list.append(address + END_TOKEN);
1164 }
1165 }
1166 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001167 }
1168 }
1169
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001170 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
1171 HashSet<String> hash = new HashSet<String>();
1172 for (Rfc822Token[] tokens : list) {
1173 for (int i = 0; i < tokens.length; i++) {
1174 hash.add(tokens[i].getAddress());
1175 }
1176 }
1177 return hash;
1178 }
1179
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001180 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1181 @VisibleForTesting
1182 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1183
1184 for (String address: addresses) {
1185 tokenized.add(Rfc822Tokenizer.tokenize(address));
1186 }
1187 return tokenized;
1188 }
1189
1190 @VisibleForTesting
1191 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1192 for (String address : addresses) {
1193 addAddressToList(address, list);
1194 }
1195 }
1196
1197 private void addAddressToList(String address, RecipientEditTextView list) {
1198 if (address == null || list == null)
1199 return;
1200
1201 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1202
1203 for (int i = 0; i < tokens.length; i++) {
1204 list.append(tokens[i] + END_TOKEN);
1205 }
1206 }
1207
1208 @VisibleForTesting
1209 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001210 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001211 // The To recipient is the reply-to address specified in the original
1212 // message, unless it is:
1213 // the current user OR a custom from of the current user, in which case
1214 // it's the To recipient list of the original message.
1215 // OR missing, in which case use the sender of the original message
1216 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001217 if (!TextUtils.isEmpty(replyToAddress)) {
1218 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001219 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001220 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001221 }
1222 return toAddresses;
1223 }
1224
1225 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
1226 for (String email : addresses) {
1227 // Do not add this account, or any of the custom froms, to the list
1228 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001229 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001230 if (!account.equalsIgnoreCase(recipientAddress)) {
1231 recipients.add(email.replace("\"\"", ""));
1232 }
1233 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001234 }
1235
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001236 private void setSubject(Message refMessage, int action) {
1237 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001238 String prefix;
1239 String correctedSubject = null;
1240 if (action == ComposeActivity.COMPOSE) {
1241 prefix = "";
1242 } else if (action == ComposeActivity.FORWARD) {
1243 prefix = getString(R.string.forward_subject_label);
1244 } else {
1245 prefix = getString(R.string.reply_subject_label);
1246 }
1247
1248 // Don't duplicate the prefix
1249 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
1250 correctedSubject = subject;
1251 } else {
1252 correctedSubject = String
1253 .format(getString(R.string.formatted_subject), prefix, subject);
1254 }
1255 mSubject.setText(correctedSubject);
1256 }
1257
Mindy Pereira818143e2012-01-11 13:59:49 -08001258 private void initRecipients() {
1259 setupRecipients(mTo);
1260 setupRecipients(mCc);
1261 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001262 }
1263
Mindy Pereira818143e2012-01-11 13:59:49 -08001264 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001265 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -08001266 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001267 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001268 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001269 int offset = accountName.indexOf("@") + 1;
1270 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001271 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001272 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001273 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001274 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001275 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001276 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001277 }
1278
1279 @Override
1280 public void onClick(View v) {
1281 int id = v.getId();
1282 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001283 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001284 // Verify that cc/ bcc aren't showing.
1285 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001286 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001287 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001288 case R.id.add_attachment:
1289 doAttach();
1290 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001291 }
1292 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001293
1294 @Override
1295 public boolean onCreateOptionsMenu(Menu menu) {
1296 super.onCreateOptionsMenu(menu);
1297 MenuInflater inflater = getMenuInflater();
1298 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001299 mSave = menu.findItem(R.id.save);
1300 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001301 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1302 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1303 if (helpItem != null) {
1304 helpItem.setVisible(mAccount != null
1305 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1306 }
1307 if (sendFeedbackItem != null) {
1308 sendFeedbackItem.setVisible(mAccount != null
1309 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1310 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001311 return true;
1312 }
1313
1314 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001315 public boolean onPrepareOptionsMenu(Menu menu) {
1316 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001317 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001318 // Its possible there is a menu item OR a button.
1319 boolean ccFieldVisible = mCc.isShown();
1320 boolean bccFieldVisible = mBcc.isShown();
1321 if (!ccFieldVisible || !bccFieldVisible) {
1322 ccBcc.setVisible(true);
1323 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1324 : R.string.add_bcc_label));
1325 } else {
1326 ccBcc.setVisible(false);
1327 }
1328 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001329 if (mSave != null) {
1330 mSave.setEnabled(shouldSave());
1331 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001332 return true;
1333 }
1334
1335 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001336 public boolean onOptionsItemSelected(MenuItem item) {
1337 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001338 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001339 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001340 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -08001341 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001342 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001343 case R.id.add_cc_bcc:
1344 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001345 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001346 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001347 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001348 break;
1349 case R.id.send:
1350 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001351 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001352 case R.id.discard:
1353 doDiscard();
1354 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001355 case R.id.settings:
1356 Utils.showSettings(this, mAccount);
1357 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001358 case android.R.id.home:
1359 finish();
1360 break;
1361 case R.id.help_info_menu_item:
1362 // TODO: enable context sensitive help
Paul Westbrook498e76d2012-04-12 16:33:02 -07001363 Utils.showHelp(this, mAccount, null);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001364 break;
1365 case R.id.feedback_menu_item:
1366 Utils.sendFeedback(this, mAccount);
1367 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001368 default:
1369 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001370 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001371 }
1372 return !handled ? super.onOptionsItemSelected(item) : handled;
1373 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001374
Mindy Pereira33fe9082012-01-09 16:24:30 -08001375 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001376 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001377 }
1378
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001379 private void doSave(boolean showToast, boolean resetIME) {
1380 sendOrSaveWithSanityChecks(true, showToast, false);
1381 if (resetIME) {
1382 // Clear the IME composing suggestions from the body.
1383 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1384 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001385 }
1386
Mindy Pereira82cc5662012-01-09 17:29:30 -08001387 /*package*/ interface SendOrSaveCallback {
1388 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001389 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1390 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001391 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1392 }
1393
1394 /*package*/ static class SendOrSaveTask implements Runnable {
1395 private final Context mContext;
1396 private final SendOrSaveCallback mSendOrSaveCallback;
1397 @VisibleForTesting
1398 final SendOrSaveMessage mSendOrSaveMessage;
1399
1400 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1401 SendOrSaveCallback callback) {
1402 mContext = context;
1403 mSendOrSaveCallback = callback;
1404 mSendOrSaveMessage = message;
1405 }
1406
1407 @Override
1408 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001409 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001410
Mindy Pereira92551d02012-04-05 11:31:12 -07001411 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001412 Message message = mSendOrSaveCallback.getMessage();
1413 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001414 // If a previous draft has been saved, in an account that is different
1415 // than what the user wants to send from, remove the old draft, and treat this
1416 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001417 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001418 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1419 ContentResolver resolver = mContext.getContentResolver();
1420 ContentValues values = new ContentValues();
1421 values.put(BaseColumns._ID, messageId);
Mindy Pereira92551d02012-04-05 11:31:12 -07001422 if (selectedAccount.account.expungeMessageUri != null) {
1423 resolver.update(selectedAccount.account.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001424 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001425 } else {
1426 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001427 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001428 // reset messageId to 0, so a new message will be created
1429 messageId = UIProvider.INVALID_MESSAGE_ID;
1430 }
1431 }
1432
1433 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001434 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001435 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001436 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001437 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1438 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001439 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001440 ContentResolver resolver = mContext.getContentResolver();
Mindy Pereira92551d02012-04-05 11:31:12 -07001441 Uri messageUri = resolver
1442 .insert(sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
1443 : selectedAccount.account.sendMessageUri,
1444 sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001445 if (sendOrSaveMessage.mSave && messageUri != null) {
1446 Cursor messageCursor = resolver.query(messageUri,
1447 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001448 if (messageCursor != null) {
1449 try {
1450 if (messageCursor.moveToFirst()) {
1451 // Broadcast notification that a new message has
1452 // been allocated
1453 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1454 new Message(messageCursor));
1455 }
1456 } finally {
1457 messageCursor.close();
1458 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001459 }
1460 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001461 }
1462
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001463 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001464 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001465 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001466 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001467 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001468 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001469 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001470 }
1471 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1472 }
1473 }
1474
1475 // Array of the outstanding send or save tasks. Access is synchronized
1476 // with the object itself
1477 /* package for testing */
1478 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
1479 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001480 private String mSignature;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001481
1482 /*package*/ static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07001483 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001484 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001485 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001486 final boolean mSave;
1487 final int mRequestId;
1488
Mindy Pereira92551d02012-04-05 11:31:12 -07001489 public SendOrSaveMessage(ReplyFromAccount account, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001490 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001491 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001492 mValues = values;
1493 mRefMessageId = refMessageId;
1494 mSave = save;
1495 mRequestId = mValues.hashCode() ^ hashCode();
1496 }
1497
1498 int requestId() {
1499 return mRequestId;
1500 }
1501 }
1502
1503 /**
1504 * Get the to recipients.
1505 */
1506 public String[] getToAddresses() {
1507 return getAddressesFromList(mTo);
1508 }
1509
1510 /**
1511 * Get the cc recipients.
1512 */
1513 public String[] getCcAddresses() {
1514 return getAddressesFromList(mCc);
1515 }
1516
1517 /**
1518 * Get the bcc recipients.
1519 */
1520 public String[] getBccAddresses() {
1521 return getAddressesFromList(mBcc);
1522 }
1523
1524 public String[] getAddressesFromList(RecipientEditTextView list) {
1525 if (list == null) {
1526 return new String[0];
1527 }
1528 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1529 int count = tokens.length;
1530 String[] result = new String[count];
1531 for (int i = 0; i < count; i++) {
1532 result[i] = tokens[i].toString();
1533 }
1534 return result;
1535 }
1536
1537 /**
1538 * Check for invalid email addresses.
1539 * @param to String array of email addresses to check.
1540 * @param wrongEmailsOut Emails addresses that were invalid.
1541 */
1542 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
1543 for (String email : to) {
1544 if (!mValidator.isValid(email)) {
1545 wrongEmailsOut.add(email);
1546 }
1547 }
1548 }
1549
1550 /**
1551 * Show an error because the user has entered an invalid recipient.
1552 * @param message
1553 */
1554 public void showRecipientErrorDialog(String message) {
1555 // Only 1 invalid recipients error dialog should be allowed up at a
1556 // time.
1557 if (mRecipientErrorDialog != null) {
1558 mRecipientErrorDialog.dismiss();
1559 }
1560 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1561 R.string.recipient_error_dialog_title)
1562 .setIconAttribute(android.R.attr.alertDialogIcon)
1563 .setCancelable(false)
1564 .setPositiveButton(
1565 R.string.ok, new Dialog.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001566 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08001567 public void onClick(DialogInterface dialog, int which) {
1568 // after the user dismisses the recipient error
1569 // dialog we want to make sure to refocus the
1570 // recipient to field so they can fix the issue
1571 // easily
1572 if (mTo != null) {
1573 mTo.requestFocus();
1574 }
1575 mRecipientErrorDialog = null;
1576 }
1577 }).show();
1578 }
1579
1580 /**
1581 * Update the state of the UI based on whether or not the current draft
1582 * needs to be saved and the message is not empty.
1583 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001584 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001585 if (mSave != null) {
1586 mSave.setEnabled((shouldSave() && !isBlank()));
1587 }
1588 }
1589
1590 /**
1591 * Returns true if we need to save the current draft.
1592 */
1593 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001594 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001595 // The message should only be saved if:
1596 // It hasn't been sent AND
1597 // Some text has been added to the message OR
1598 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001599 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -08001600 (mReplyFromChanged && !isBlank()));
1601 }
1602 }
1603
1604 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001605 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001606 * @return boolean
1607 */
1608 public boolean isBlank() {
1609 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001610 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1611 mBodyView.getText().toString()) == 0)
1612 && mTo.length() == 0
1613 && mCc.length() == 0 && mBcc.length() == 0
1614 && mAttachmentsView.getAttachments().size() == 0;
1615 }
1616
1617 @VisibleForTesting
1618 protected int getSignatureStartPosition(String signature, String bodyText) {
1619 int startPos = -1;
1620
1621 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1622 return startPos;
1623 }
1624
1625 int bodyLength = bodyText.length();
1626 int signatureLength = signature.length();
1627 String printableVersion = convertToPrintableSignature(signature);
1628 int printableLength = printableVersion.length();
1629
1630 if (bodyLength >= printableLength
1631 && bodyText.substring(bodyLength - printableLength)
1632 .equals(printableVersion)) {
1633 startPos = bodyLength - printableLength;
1634 } else if (bodyLength >= signatureLength
1635 && bodyText.substring(bodyLength - signatureLength)
1636 .equals(signature)) {
1637 startPos = bodyLength - signatureLength;
1638 }
1639 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001640 }
1641
1642 /**
1643 * Allows any changes made by the user to be ignored. Called when the user
1644 * decides to discard a draft.
1645 */
1646 private void discardChanges() {
1647 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001648 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001649 mReplyFromChanged = false;
1650 }
1651
1652 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001653 * @param body
1654 * @param save
1655 * @param showToast
1656 * @return Whether the send or save succeeded.
1657 */
1658 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1659 final boolean orientationChanged) {
1660 String[] to, cc, bcc;
1661 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001662
Mindy Pereira181df782012-03-01 13:32:44 -08001663 if (orientationChanged) {
1664 to = cc = bcc = new String[0];
1665 } else {
1666 to = getToAddresses();
1667 cc = getCcAddresses();
1668 bcc = getBccAddresses();
1669 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001670
Mindy Pereira181df782012-03-01 13:32:44 -08001671 // Don't let the user send to nobody (but it's okay to save a message
1672 // with no recipients)
1673 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1674 showRecipientErrorDialog(getString(R.string.recipient_needed));
1675 return false;
1676 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001677
Mindy Pereira181df782012-03-01 13:32:44 -08001678 List<String> wrongEmails = new ArrayList<String>();
1679 if (!save) {
1680 checkInvalidEmails(to, wrongEmails);
1681 checkInvalidEmails(cc, wrongEmails);
1682 checkInvalidEmails(bcc, wrongEmails);
1683 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001684
Mindy Pereira181df782012-03-01 13:32:44 -08001685 // Don't let the user send an email with invalid recipients
1686 if (wrongEmails.size() > 0) {
1687 String errorText = String.format(getString(R.string.invalid_recipient),
1688 wrongEmails.get(0));
1689 showRecipientErrorDialog(errorText);
1690 return false;
1691 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001692
Mindy Pereira181df782012-03-01 13:32:44 -08001693 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001694 @Override
Mindy Pereira181df782012-03-01 13:32:44 -08001695 public void onClick(DialogInterface dialog, int which) {
1696 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1697 }
1698 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001699
Mindy Pereira181df782012-03-01 13:32:44 -08001700 // Show a warning before sending only if there are no attachments.
1701 if (!save) {
1702 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1703 boolean warnAboutEmptySubject = isSubjectEmpty();
1704 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001705
Mindy Pereira181df782012-03-01 13:32:44 -08001706 // A warning about an empty body may not be warranted when
1707 // forwarding mails, since a common use case is to forward
1708 // quoted text and not append any more text.
1709 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001710
Mindy Pereira181df782012-03-01 13:32:44 -08001711 // When we bring up a dialog warning the user about a send,
1712 // assume that they accept sending the message. If they do not,
1713 // the dialog listener is required to enable sending again.
1714 if (warnAboutEmptySubject) {
1715 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1716 return true;
1717 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001718
Mindy Pereira181df782012-03-01 13:32:44 -08001719 if (warnAboutEmptyBody) {
1720 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1721 return true;
1722 }
1723 }
1724 // Ask for confirmation to send (if always required)
1725 if (showSendConfirmation()) {
1726 showSendConfirmDialog(R.string.confirm_send_message, listener);
1727 return true;
1728 }
1729 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001730
Mindy Pereira181df782012-03-01 13:32:44 -08001731 sendOrSave(body, save, showToast, false);
1732 return true;
1733 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001734
Mindy Pereira181df782012-03-01 13:32:44 -08001735 /**
1736 * Returns a boolean indicating whether warnings should be shown for empty
1737 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07001738 *
Mindy Pereira181df782012-03-01 13:32:44 -08001739 * @return True if a warning should be shown for empty text fields
1740 */
1741 protected boolean showEmptyTextWarnings() {
1742 return mAttachmentsView.getAttachments().size() == 0;
1743 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001744
Mindy Pereira181df782012-03-01 13:32:44 -08001745 /**
1746 * Returns a boolean indicating whether the user should confirm each send
1747 *
1748 * @return True if a warning should be on each send
1749 */
1750 protected boolean showSendConfirmation() {
1751 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1752 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001753
Mindy Pereira181df782012-03-01 13:32:44 -08001754 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1755 if (mSendConfirmDialog != null) {
1756 mSendConfirmDialog.dismiss();
1757 mSendConfirmDialog = null;
1758 }
1759 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1760 .setTitle(R.string.confirm_send_title)
1761 .setIconAttribute(android.R.attr.alertDialogIcon)
1762 .setPositiveButton(R.string.send, listener)
1763 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1764 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001765
Mindy Pereira181df782012-03-01 13:32:44 -08001766 /**
1767 * Returns whether the ComposeArea believes there is any text in the body of
1768 * the composition. TODO: When ComposeArea controls the Body as well, add
1769 * that here.
1770 */
1771 public boolean isBodyEmpty() {
1772 return !mQuotedTextView.isTextIncluded();
1773 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001774
Mindy Pereira181df782012-03-01 13:32:44 -08001775 /**
1776 * Test to see if the subject is empty.
1777 *
1778 * @return boolean.
1779 */
1780 // TODO: this will likely go away when composeArea.focus() is implemented
1781 // after all the widget control is moved over.
1782 public boolean isSubjectEmpty() {
1783 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1784 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001785
Mindy Pereira181df782012-03-01 13:32:44 -08001786 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001787 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07001788 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
1789 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001790 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001791
Mindy Pereirac2031972012-04-03 09:38:35 -07001792 String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
1793
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001794 MessageModification.putToAddresses(values, message.getToAddresses());
1795 MessageModification.putCcAddresses(values, message.getCcAddresses());
1796 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001797
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001798 MessageModification.putCustomFromAddress(values, message.from);
Mindy Pereira92551d02012-04-05 11:31:12 -07001799
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001800 MessageModification.putSubject(values, message.subject);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001801 String htmlBody = Html.toHtml(body);
Paul Westbrook05b92b82012-04-20 13:29:37 -07001802
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001803 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1804 StringBuilder fullBody = new StringBuilder(htmlBody);
1805 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001806 // HTML gets converted to text for now
1807 final String text = quotedText.toString();
1808 if (QuotedTextView.containsQuotedText(text)) {
1809 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07001810 final int quoteStartPos = fullBody.length() + pos;
1811 fullBody.append(text);
1812 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07001813 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07001814 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001815 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001816 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1817 // This shouldn't happen, but just use what we have,
1818 // and don't do server-side expansion
1819 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001820 }
1821 }
Mindy Pereira12575862012-03-21 16:30:54 -07001822 int draftType = -1;
1823 switch (composeMode) {
1824 case ComposeActivity.COMPOSE:
1825 draftType = DraftType.COMPOSE;
1826 break;
1827 case ComposeActivity.REPLY:
1828 draftType = DraftType.REPLY;
1829 break;
1830 case ComposeActivity.REPLY_ALL:
1831 draftType = DraftType.REPLY_ALL;
1832 break;
1833 case ComposeActivity.FORWARD:
1834 draftType = DraftType.FORWARD;
1835 break;
1836 }
1837 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07001838 if (refMessage != null) {
1839 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1840 MessageModification.putBodyHtml(values, fullBody.toString());
1841 }
1842 if (!TextUtils.isEmpty(refMessage.bodyText)) {
1843 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1844 }
1845 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07001846 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07001847 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1848 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001849 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07001850 if (!TextUtils.isEmpty(refMessageId)) {
1851 MessageModification.putRefMessageId(values, refMessageId);
1852 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001853
Mindy Pereira92551d02012-04-05 11:31:12 -07001854 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(replyFromAccount,
Mindy Pereira181df782012-03-01 13:32:44 -08001855 values, refMessageId, save);
1856 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001857
Mindy Pereira181df782012-03-01 13:32:44 -08001858 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001859
Mindy Pereira181df782012-03-01 13:32:44 -08001860 // Do the send/save action on the specified handler to avoid possible
1861 // ANRs
1862 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001863
Mindy Pereira181df782012-03-01 13:32:44 -08001864 return sendOrSaveMessage.requestId();
1865 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001866
Mindy Pereira181df782012-03-01 13:32:44 -08001867 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1868 boolean orientationChanged) {
1869 // Check if user is a monkey. Monkeys can compose and hit send
1870 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07001871 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08001872 return;
1873 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001874
Mindy Pereira181df782012-03-01 13:32:44 -08001875 String[] to, cc, bcc;
1876 if (orientationChanged) {
1877 to = cc = bcc = new String[0];
1878 } else {
1879 to = getToAddresses();
1880 cc = getCcAddresses();
1881 bcc = getBccAddresses();
1882 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001883
Mindy Pereira181df782012-03-01 13:32:44 -08001884 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001885 private int mRestoredRequestId;
1886
Marc Blank0bbc8582012-04-23 15:07:57 -07001887 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08001888 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08001889 synchronized (mActiveTasks) {
1890 int numTasks = mActiveTasks.size();
1891 if (numTasks == 0) {
1892 // Start service so we won't be killed if this app is
1893 // put in the background.
1894 startService(new Intent(ComposeActivity.this, EmptyService.class));
1895 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001896
Mindy Pereira181df782012-03-01 13:32:44 -08001897 mActiveTasks.add(sendOrSaveTask);
1898 }
1899 if (sTestSendOrSaveCallback != null) {
1900 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1901 }
1902 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001903
Marc Blank0bbc8582012-04-23 15:07:57 -07001904 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001905 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1906 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08001907 synchronized (mDraftLock) {
1908 mDraftId = message.id;
1909 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001910 if (sRequestMessageIdMap != null) {
1911 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1912 }
Mindy Pereira181df782012-03-01 13:32:44 -08001913 // Cache request message map, in case the process is killed
1914 saveRequestMap();
1915 }
1916 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001917 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08001918 }
1919 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001920
Marc Blank0bbc8582012-04-23 15:07:57 -07001921 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001922 public Message getMessage() {
1923 synchronized (mDraftLock) {
1924 return mDraft;
1925 }
1926 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001927
Marc Blank0bbc8582012-04-23 15:07:57 -07001928 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001929 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1930 if (success) {
1931 // Successfully sent or saved so reset change markers
1932 discardChanges();
1933 } else {
1934 // A failure happened with saving/sending the draft
1935 // TODO(pwestbro): add a better string that should be used
1936 // when failing to send or save
1937 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1938 .show();
1939 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001940
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001941 int numTasks;
1942 synchronized (mActiveTasks) {
1943 // Remove the task from the list of active tasks
1944 mActiveTasks.remove(task);
1945 numTasks = mActiveTasks.size();
1946 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001947
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001948 if (numTasks == 0) {
1949 // Stop service so we can be killed.
1950 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1951 }
1952 if (sTestSendOrSaveCallback != null) {
1953 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1954 }
1955 }
Mindy Pereira181df782012-03-01 13:32:44 -08001956 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001957
Mindy Pereira181df782012-03-01 13:32:44 -08001958 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07001959 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08001960 String fromAddress = selectedAccount.name;
1961 if (selectedAccount == null || fromAddress == null) {
1962 // We don't have either the selected account or from address,
1963 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07001964 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08001965 fromAddress = mAccount.name;
1966 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001967
Mindy Pereira181df782012-03-01 13:32:44 -08001968 if (mSendSaveTaskHandler == null) {
1969 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1970 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001971
Mindy Pereira181df782012-03-01 13:32:44 -08001972 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1973 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001974
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001975 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07001976 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
1977 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Mindy Pereira12575862012-03-21 16:30:54 -07001978 mSendSaveTaskHandler, save, mComposeMode);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001979
Mindy Pereira181df782012-03-01 13:32:44 -08001980 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1981 mRecipient = selectedAccount.name;
1982 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07001983 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001984
Mindy Pereira181df782012-03-01 13:32:44 -08001985 // Don't display the toast if the user is just changing the orientation,
1986 // but we still need to save the draft to the cursor because this is how we restore
1987 // the attachments when the configuration change completes.
1988 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1989 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1990 Toast.LENGTH_LONG).show();
1991 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001992
Mindy Pereira181df782012-03-01 13:32:44 -08001993 // Need to update variables here because the send or save completes
1994 // asynchronously even though the toast shows right away.
1995 discardChanges();
1996 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001997
Mindy Pereira181df782012-03-01 13:32:44 -08001998 // If we are sending, finish the activity
1999 if (!save) {
2000 finish();
2001 }
2002 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002003
Mindy Pereira181df782012-03-01 13:32:44 -08002004 /**
2005 * Save the state of the request messageid map. This allows for the Gmail
2006 * process to be killed, but and still allow for ComposeActivity instances
2007 * to be recreated correctly.
2008 */
2009 private void saveRequestMap() {
2010 // TODO: store the request map in user preferences.
2011 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002012
Mindy Pereira013194c2012-01-06 15:09:33 -08002013 public void doAttach() {
2014 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2015 i.addCategory(Intent.CATEGORY_OPENABLE);
Mindy Pereira181df782012-03-01 13:32:44 -08002016 if (android.provider.Settings.System.getInt(getContentResolver(),
2017 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002018 i.setType("*/*");
2019 } else {
2020 i.setType("image/*");
2021 }
2022 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002023 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2024 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002025 }
2026
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002027 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002028 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002029 if (mCcBccButton != null) {
2030 mCcBccButton.setVisibility(View.GONE);
2031 }
2032 }
2033
Mindy Pereira326c6602012-01-04 15:32:42 -08002034 @Override
2035 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002036 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002037 if (position == ComposeActivity.REPLY) {
2038 mComposeMode = ComposeActivity.REPLY;
2039 } else if (position == ComposeActivity.REPLY_ALL) {
2040 mComposeMode = ComposeActivity.REPLY_ALL;
2041 } else if (position == ComposeActivity.FORWARD) {
2042 mComposeMode = ComposeActivity.FORWARD;
2043 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002044 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002045 resetMessageForModeChange();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002046 if (mRefMessage != null) {
2047 initFromRefMessage(mComposeMode, mAccount.name);
2048 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002049 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002050 return true;
2051 }
2052
Mindy Pereira154386a2012-01-11 13:02:33 -08002053 private void resetMessageForModeChange() {
2054 // When switching between reply, reply all, forward,
2055 // follow the behavior of webview.
2056 // The contents of the following fields are cleared
2057 // so that they can be populated directly from the
2058 // ref message:
2059 // 1) Any recipient fields
2060 // 2) The subject
2061 mTo.setText("");
2062 mCc.setText("");
2063 mBcc.setText("");
2064 // Any edits to the subject are replaced with the original subject.
2065 mSubject.setText("");
2066
2067 // Any changes to the contents of the following fields are kept:
2068 // 1) Body
2069 // 2) Attachments
2070 // If the user made changes to attachments, keep their changes.
2071 if (!mAttachmentsChanged) {
2072 mAttachmentsView.deleteAllAttachments();
2073 }
2074 }
2075
Mindy Pereira326c6602012-01-04 15:32:42 -08002076 private class ComposeModeAdapter extends ArrayAdapter<String> {
2077
2078 private LayoutInflater mInflater;
2079
2080 public ComposeModeAdapter(Context context) {
2081 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2082 .getStringArray(R.array.compose_modes));
2083 }
2084
2085 private LayoutInflater getInflater() {
2086 if (mInflater == null) {
2087 mInflater = LayoutInflater.from(getContext());
2088 }
2089 return mInflater;
2090 }
2091
2092 @Override
2093 public View getView(int position, View convertView, ViewGroup parent) {
2094 if (convertView == null) {
2095 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2096 }
2097 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2098 return super.getView(position, convertView, parent);
2099 }
2100 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002101
2102 @Override
2103 public void onRespondInline(String text) {
2104 appendToBody(text, false);
2105 }
2106
2107 /**
2108 * Append text to the body of the message. If there is no existing body
2109 * text, just sets the body to text.
2110 *
2111 * @param text
2112 * @param withSignature True to append a signature.
2113 */
2114 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002115 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002116 if (bodyText != null && bodyText.length() > 0) {
2117 bodyText.append(text);
2118 } else {
2119 setBody(text, withSignature);
2120 }
2121 }
2122
2123 /**
2124 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002125 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002126 * @param text
2127 * @param withSignature True to append a signature.
2128 */
2129 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002130 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002131 if (withSignature) {
2132 appendSignature();
2133 }
2134 }
2135
2136 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002137 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002138 boolean hasFocus = mBodyView.hasFocus();
Mindy Pereirab13917c2012-03-29 08:08:19 -07002139 if (!TextUtils.equals(newSignature, mSignature)) {
2140 mSignature = newSignature;
2141 if (!TextUtils.isEmpty(mSignature)
2142 && getSignatureStartPosition(mSignature,
2143 mBodyView.getText().toString()) < 0) {
2144 // Appending a signature does not count as changing text.
2145 mBodyView.removeTextChangedListener(this);
2146 mBodyView.append(convertToPrintableSignature(mSignature));
2147 mBodyView.addTextChangedListener(this);
2148 }
Mindy Pereira433b1982012-04-03 11:53:07 -07002149 if (hasFocus) {
2150 focusBody();
2151 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002152 }
2153 }
2154
2155 private String convertToPrintableSignature(String signature) {
2156 String signatureResource = getResources().getString(R.string.signature);
2157 if (signature == null) {
2158 signature = "";
2159 }
2160 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002161 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002162
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08002163 @Override
2164 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07002165 mReplyFromAccount = mFromSpinner.getCurrentAccount();
2166 if (!mAccount.equals(mReplyFromAccount.account)) {
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002167 setAccount(mReplyFromAccount.account);
2168
Mindy Pereira181df782012-03-01 13:32:44 -08002169 // TODO: handle discarding attachments when switching accounts.
2170 // Only enable save for this draft if there is any other content
2171 // in the message.
2172 if (!isBlank()) {
2173 enableSave(true);
2174 }
2175 mReplyFromChanged = true;
2176 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002177 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002178 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002179
2180 public void enableSave(boolean enabled) {
2181 if (mSave != null) {
2182 mSave.setEnabled(enabled);
2183 }
2184 }
2185
2186 public void enableSend(boolean enabled) {
2187 if (mSend != null) {
2188 mSend.setEnabled(enabled);
2189 }
2190 }
2191
2192 /**
2193 * Handles button clicks from any error dialogs dealing with sending
2194 * a message.
2195 */
2196 @Override
2197 public void onClick(DialogInterface dialog, int which) {
2198 switch (which) {
2199 case DialogInterface.BUTTON_POSITIVE: {
2200 doDiscardWithoutConfirmation(true /* show toast */ );
2201 break;
2202 }
2203 case DialogInterface.BUTTON_NEGATIVE: {
2204 // If the user cancels the send, re-enable the send button.
2205 enableSend(true);
2206 break;
2207 }
2208 }
2209
2210 }
2211
Mindy Pereiraefe3d252012-03-01 14:20:44 -08002212 private void doDiscard() {
2213 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2214 .setPositiveButton(R.string.ok, this)
2215 .setNegativeButton(R.string.cancel, null)
2216 .create().show();
2217 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002218 /**
2219 * Effectively discard the current message.
2220 *
2221 * This method is either invoked from the menu or from the dialog
2222 * once the user has confirmed that they want to discard the message.
2223 * @param showToast show "Message discarded" toast if true
2224 */
2225 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002226 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002227 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2228 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07002229 values.put(BaseColumns._ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002230 if (mAccount.expungeMessageUri != null) {
2231 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2232 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07002233 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002234 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002235 // This is not strictly necessary (since we should not try to
2236 // save the draft after calling this) but it ensures that if we
2237 // do save again for some reason we make a new draft rather than
2238 // trying to resave an expunged draft.
2239 mDraftId = UIProvider.INVALID_MESSAGE_ID;
2240 }
2241 }
2242
2243 if (showToast) {
2244 // Display a toast to let the user know
2245 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2246 }
2247
2248 // This prevents the draft from being saved in onPause().
2249 discardChanges();
2250 finish();
2251 }
2252
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002253 private void saveIfNeeded() {
2254 if (mAccount == null) {
2255 // We have not chosen an account yet so there's no way that we can save. This is ok,
2256 // though, since we are saving our state before AccountsActivity is activated. Thus, the
2257 // user has not interacted with us yet and there is no real state to save.
2258 return;
2259 }
2260
2261 if (shouldSave()) {
2262 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
2263 }
2264 }
2265
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002266 @Override
2267 public void onAttachmentDeleted() {
2268 mAttachmentsChanged = true;
2269 updateSaveUi();
2270 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002271
2272
2273 /**
2274 * This is called any time one of our text fields changes.
2275 */
Marc Blank0bbc8582012-04-23 15:07:57 -07002276 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002277 public void afterTextChanged(Editable s) {
2278 mTextChanged = true;
2279 updateSaveUi();
2280 }
2281
2282 @Override
2283 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2284 // Do nothing.
2285 }
2286
Marc Blank0bbc8582012-04-23 15:07:57 -07002287 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002288 public void onTextChanged(CharSequence s, int start, int before, int count) {
2289 // Do nothing.
2290 }
2291
2292
2293 // There is a big difference between the text associated with an address changing
2294 // to add the display name or to format properly and a recipient being added or deleted.
2295 // Make sure we only notify of changes when a recipient has been added or deleted.
2296 private class RecipientTextWatcher implements TextWatcher {
2297 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2298
2299 private RecipientEditTextView mView;
2300
2301 private TextWatcher mListener;
2302
2303 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2304 mView = view;
2305 mListener = listener;
2306 }
2307
2308 @Override
2309 public void afterTextChanged(Editable s) {
2310 if (hasChanged()) {
2311 mListener.afterTextChanged(s);
2312 }
2313 }
2314
2315 private boolean hasChanged() {
2316 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2317 int totalCount = currRecips.length;
2318 int totalPrevCount = 0;
2319 for (Entry<String, Integer> entry : mContent.entrySet()) {
2320 totalPrevCount += entry.getValue();
2321 }
2322 if (totalCount != totalPrevCount) {
2323 return true;
2324 }
2325
2326 for (String recip : currRecips) {
2327 if (!mContent.containsKey(recip)) {
2328 return true;
2329 } else {
2330 int count = mContent.get(recip) - 1;
2331 if (count < 0) {
2332 return true;
2333 } else {
2334 mContent.put(recip, count);
2335 }
2336 }
2337 }
2338 return false;
2339 }
2340
2341 private String[] tokenizeRecips(String[] recips) {
2342 // Tokenize them all and put them in the list.
2343 String[] recipAddresses = new String[recips.length];
2344 for (int i = 0; i < recips.length; i++) {
2345 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2346 }
2347 return recipAddresses;
2348 }
2349
2350 @Override
2351 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2352 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2353 for (String recip : recips) {
2354 if (!mContent.containsKey(recip)) {
2355 mContent.put(recip, 1);
2356 } else {
2357 mContent.put(recip, (mContent.get(recip)) + 1);
2358 }
2359 }
2360 }
2361
2362 @Override
2363 public void onTextChanged(CharSequence s, int start, int before, int count) {
2364 // Do nothing.
2365 }
2366 }
Vikram Aggarwal8183d452012-04-17 09:13:29 -07002367}