blob: d038a7bd2407299bca7b01747689c5a20413ba34 [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
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700102 static final int COMPOSE = -1;
103 static final int REPLY = 0;
104 static final int REPLY_ALL = 1;
105 static final int FORWARD = 2;
106 static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800107
108 // Integer extra holding one of the above compose action
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700109 private static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800110
Mindy Pereira326689d2012-05-17 10:14:14 -0700111 private static final String EXTRA_SHOW_CC = "showCc";
112 private static final String EXTRA_SHOW_BCC = "showBcc";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700113
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700114 private static final String UTF8_ENCODING_NAME = "UTF-8";
115
116 private static final String MAIL_TO = "mailto";
117
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700118 private static final String EXTRA_SUBJECT = "subject";
119
120 private static final String EXTRA_BODY = "body";
121
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700122 private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
123
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700124 // Extra that we can get passed from other activities
125 private static final String EXTRA_TO = "to";
126 private static final String EXTRA_CC = "cc";
127 private static final String EXTRA_BCC = "bcc";
128
129 // List of all the fields
130 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
131
Mindy Pereira82cc5662012-01-09 17:29:30 -0800132 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
133 // Map containing information about requests to create new messages, and the id of the
134 // messages that were the result of those requests.
135 //
136 // This map is used when the activity that initiated the save a of a new message, is killed
137 // before the save has completed (and when we know the id of the newly created message). When
138 // a save is completed, the service that is running in the background, will update the map
139 //
140 // When a new ComposeActivity instance is created, it will attempt to use the information in
141 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
142 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
143 // to populate the map with data stored in shared preferences.
144 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
145 // Key used to store the above map
146 private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
Mindy Pereira6349a042012-01-04 11:25:01 -0800147 /**
148 * Notifies the {@code Activity} that the caller is an Email
149 * {@code Activity}, so that the back behavior may be modified accordingly.
150 *
151 * @see #onAppUpPressed
152 */
153 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
154
Paul Westbrookf97588b2012-03-20 11:11:37 -0700155 static final String EXTRA_ATTACHMENTS = "attachments";
156
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800157 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700158 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700159 // If this is an action to edit an existing draft messagge, this extra will hold the
160 // draft message
161 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800162 private static final String END_TOKEN = ", ";
Mindy Pereira013194c2012-01-06 15:09:33 -0800163 private static final String LOG_TAG = new LogUtils().getLogTag();
164 // Request numbers for activities we start
165 private static final int RESULT_PICK_ATTACHMENT = 1;
166 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700167 // TODO(mindyp) set mime-type for auto send?
168 private static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
169
170 // Max size for attachments (5 megs). Will be overridden by account settings if found.
171 // TODO(mindyp): read this from account settings?
172 private static final int DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700173 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
174 private static final String EXTRA_REQUEST_ID = "requestId";
175 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
176 private static final String EXTRA_FOCUS_SELECTION_END = null;
177 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800178
Mindy Pereira82cc5662012-01-09 17:29:30 -0800179 /**
180 * A single thread for running tasks in the background.
181 */
182 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800183 private RecipientEditTextView mTo;
184 private RecipientEditTextView mCc;
185 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800186 private Button mCcBccButton;
187 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800188 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800189 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700190 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800191 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800192 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800193 private TextView mSubject;
194
Mindy Pereira326c6602012-01-04 15:32:42 -0800195 private ComposeModeAdapter mComposeModeAdapter;
196 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800197 private boolean mForward;
198 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800199 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700200 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800201 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800202 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800203 private View mFromSpinnerWrapper;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800204 private FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800205 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800206 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800207 private boolean mTextChanged;
208 private boolean mReplyFromChanged;
209 private MenuItem mSave;
210 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800211 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800212 private AlertDialog mSendConfirmDialog;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800213 private Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800214 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
215 private Message mDraft;
216 private Object mDraftLock = new Object();
Mindy Pereira1f936682012-03-02 11:30:33 -0800217 private ImageView mAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800218
Mindy Pereira326c6602012-01-04 15:32:42 -0800219 /**
220 * Can be called from a non-UI thread.
221 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800222 public static void editDraft(Context launcher, Account account, Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700223 launch(launcher, account, message, EDIT_DRAFT);
Mindy Pereira326c6602012-01-04 15:32:42 -0800224 }
225
Mindy Pereira6349a042012-01-04 11:25:01 -0800226 /**
227 * Can be called from a non-UI thread.
228 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800229 public static void compose(Context launcher, Account account) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800230 launch(launcher, account, null, COMPOSE);
231 }
232
233 /**
234 * Can be called from a non-UI thread.
235 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800236 public static void reply(Context launcher, Account account, Message message) {
237 launch(launcher, account, message, REPLY);
Mindy Pereira6349a042012-01-04 11:25:01 -0800238 }
239
240 /**
241 * Can be called from a non-UI thread.
242 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800243 public static void replyAll(Context launcher, Account account, Message message) {
244 launch(launcher, account, message, REPLY_ALL);
Mindy Pereira6349a042012-01-04 11:25:01 -0800245 }
246
247 /**
248 * Can be called from a non-UI thread.
249 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800250 public static void forward(Context launcher, Account account, Message message) {
251 launch(launcher, account, message, FORWARD);
Mindy Pereira6349a042012-01-04 11:25:01 -0800252 }
253
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800254 private static void launch(Context launcher, Account account, Message message, int action) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800255 Intent intent = new Intent(launcher, ComposeActivity.class);
256 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
257 intent.putExtra(EXTRA_ACTION, action);
258 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700259 if (action == EDIT_DRAFT) {
260 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
261 } else {
262 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
263 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800264 launcher.startActivity(intent);
265 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800266
267 @Override
268 public void onCreate(Bundle savedInstanceState) {
269 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800270 setContentView(R.layout.compose);
271 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800272 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700273 Account account;
274 Message message;
Mindy Pereira71c9e562012-05-17 11:01:02 -0700275 boolean showQuotedText = false;
276
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700277 int action;
278 if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE)) {
279 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
280 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
281 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
282 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
283 } else {
284 account = (Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
285 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
286 // Initialize the message from the message in the intent
287 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
288 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
289 }
Paul Westbrook92227f62012-03-20 10:32:51 -0700290 if (account == null) {
291 final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
292 if (syncingAccounts.length > 0) {
293 account = syncingAccounts[0];
294 }
295 }
296
297 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800298 if (mAccount == null) {
299 return;
300 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700301
302 if (message != null && action != EDIT_DRAFT) {
303 initFromDraftMessage(message);
304 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700305 showCcBcc(savedInstanceState);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700306 showQuotedText = message.appendRefMessageContent;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700307 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700308 initFromDraftMessage(message);
Mindy Pereira326689d2012-05-17 10:14:14 -0700309 showCcBcc(message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700310 // Update the action to the draft type of the previous draft
311 switch (message.draftType) {
312 case UIProvider.DraftType.REPLY:
313 action = REPLY;
314 break;
315 case UIProvider.DraftType.REPLY_ALL:
316 action = REPLY_ALL;
317 break;
318 case UIProvider.DraftType.FORWARD:
319 action = FORWARD;
320 break;
321 case UIProvider.DraftType.COMPOSE:
322 default:
323 action = COMPOSE;
324 break;
325 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700326 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700327 showQuotedText = message.appendRefMessageContent;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700328 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
329 if (mRefMessage != null) {
330 initFromRefMessage(action, mAccount.name);
Mindy Pereira326689d2012-05-17 10:14:14 -0700331 showCcBcc(mRefMessage);
Mindy Pereira71c9e562012-05-17 11:01:02 -0700332 showQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700333 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700334 } else {
335 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700336 }
337
338 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800339 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800340 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800341 initRecipients();
Paul Westbrookf97588b2012-03-20 11:11:37 -0700342 initAttachmentsFromIntent(intent);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800343 initActionBar(action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700344 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
345 action);
Mindy Pereira75f66632012-01-11 11:42:02 -0800346 initChangeListeners();
Mindy Pereira433b1982012-04-03 11:53:07 -0700347 setFocus(action);
Mindy Pereira326689d2012-05-17 10:14:14 -0700348 updateHideOrShowCcBcc();
Mindy Pereira71c9e562012-05-17 11:01:02 -0700349 updateHideOrShowQuotedText(showQuotedText);
350 }
351
352 private void updateHideOrShowQuotedText(boolean showQuotedText) {
353 mQuotedTextView.updateCheckedState(showQuotedText);
Mindy Pereira433b1982012-04-03 11:53:07 -0700354 }
355
356 private void setFocus(int action) {
357 if (action == EDIT_DRAFT) {
358 int type = mDraft.draftType;
359 switch (type) {
360 case UIProvider.DraftType.COMPOSE:
361 case UIProvider.DraftType.FORWARD:
362 action = COMPOSE;
363 break;
364 case UIProvider.DraftType.REPLY:
365 case UIProvider.DraftType.REPLY_ALL:
366 default:
367 action = REPLY;
368 break;
369 }
370 }
371 switch (action) {
372 case FORWARD:
373 case COMPOSE:
374 mTo.requestFocus();
375 break;
376 case REPLY:
377 case REPLY_ALL:
378 default:
379 focusBody();
380 break;
381 }
382 }
383
384 /**
385 * Focus the body of the message.
386 */
387 public void focusBody() {
388 mBodyView.requestFocus();
389 int length = mBodyView.getText().length();
390
391 int signatureStartPos = getSignatureStartPosition(
392 mSignature, mBodyView.getText().toString());
393 if (signatureStartPos > -1) {
394 // In case the user deleted the newlines...
395 mBodyView.setSelection(signatureStartPos);
396 } else if (length > 0) {
397 // Move cursor to the end.
398 mBodyView.setSelection(length);
399 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800400 }
401
402 @Override
403 protected void onResume() {
404 super.onResume();
405 // Update the from spinner as other accounts
406 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800407 if (mFromSpinner != null && mAccount != null) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700408 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount);
Mindy Pereira818143e2012-01-11 13:59:49 -0800409 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800410 }
411
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800412 @Override
413 protected void onPause() {
414 super.onPause();
415
416 if (mSendConfirmDialog != null) {
417 mSendConfirmDialog.dismiss();
418 }
419 if (mRecipientErrorDialog != null) {
420 mRecipientErrorDialog.dismiss();
421 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800422 saveIfNeeded();
423 }
424
425 @Override
426 protected final void onActivityResult(int request, int result, Intent data) {
427 mAddingAttachment = false;
428
429 if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
430 addAttachmentAndUpdateView(data);
431 }
432 }
433
434 @Override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700435 public final void onRestoreInstanceState(Bundle savedInstanceState) {
436 super.onRestoreInstanceState(savedInstanceState);
437 if (savedInstanceState != null) {
438 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
439 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
440 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
441 // There should be a focus and it should be an EditText since we
442 // only save these extras if these conditions are true.
443 EditText focusEditText = (EditText) getCurrentFocus();
444 final int length = focusEditText.getText().length();
445 if (selectionStart < length && selectionEnd < length) {
446 focusEditText.setSelection(selectionStart, selectionEnd);
447 }
448 }
449 }
450 }
451
452 @Override
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800453 public final void onSaveInstanceState(Bundle state) {
454 super.onSaveInstanceState(state);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700455 // The framework is happy to save and restore the selection but only if it also saves and
456 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
457 // this manually.
458 View focus = getCurrentFocus();
459 if (focus != null && focus instanceof EditText) {
460 EditText focusEditText = (EditText) focus;
461 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
462 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
463 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700464
465 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700466 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Paul Westbrook6273e962012-04-23 10:44:15 -0700467 final ReplyFromAccount selectedReplyFromAccount =
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700468 (replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
469 replyFromAccounts.get(selectedPos) :
Paul Westbrook6273e962012-04-23 10:44:15 -0700470 null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700471 if (selectedReplyFromAccount != null) {
472 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
473 .toString());
474 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
475 } else {
476 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
477 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800478
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700479 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
480 // We don't have a draft id, and we have a request id,
481 // save the request id.
482 state.putInt(EXTRA_REQUEST_ID, mRequestId);
483 }
484
485 // We want to restore the current mode after a pause
486 // or rotation.
487 int mode = getMode();
488 state.putInt(EXTRA_ACTION, mode);
489
490 Message message = createMessage(selectedReplyFromAccount, mode);
491 state.putParcelable(EXTRA_MESSAGE, message);
492
493 if (mRefMessage != null) {
494 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
495 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700496 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
497 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700498 }
499
500 private int getMode() {
501 int mode = ComposeActivity.COMPOSE;
502 ActionBar actionBar = getActionBar();
503 if (actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
504 mode = actionBar.getSelectedNavigationIndex();
505 }
506 return mode;
507 }
508
509 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
510 Message message = new Message();
511 message.id = UIProvider.INVALID_MESSAGE_ID;
512 message.serverId =UIProvider.INVALID_MESSAGE_ID;
513 message.uri = null;
514 message.conversationUri = null;
515 message.subject = mSubject.getText().toString();
516 message.snippet = null;
Paul Westbrook91906812012-04-25 11:03:27 -0700517 message.from = selectedReplyFromAccount != null ?
518 selectedReplyFromAccount.name : mAccount.name;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700519 message.to = mTo.getText().toString();
Mindy Pereira4b1377e2012-04-18 15:08:05 -0700520 message.cc = mCc.getText().toString();
521 message.bcc = mBcc.getText().toString();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700522 message.replyTo = null;
523 message.dateReceivedMs = 0;
524 String htmlBody = Html.toHtml(mBodyView.getText());
525 StringBuilder fullBody = new StringBuilder(htmlBody);
526 message.bodyHtml = fullBody.toString();
527 message.bodyText = mBodyView.getText().toString();
528 message.embedsExternalResources = false;
529 message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
530 message.draftType = mode;
531 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
532 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
533 message.hasAttachments = attachments != null && attachments.size() > 0;
534 message.attachmentListUri = null;
535 message.messageFlags = 0;
536 message.saveUri = null;
537 message.sendUri = null;
538 message.alwaysShowImages = false;
539 message.attachmentsJson = Attachment.toJSONArray(attachments);
540 CharSequence quotedText = mQuotedTextView.getQuotedText();
541 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
542 .getQuotedTextOffset(quotedText.toString()) : -1;
543 message.accountUri = null;
544 return message;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800545 }
546
Mindy Pereira818143e2012-01-11 13:59:49 -0800547 @VisibleForTesting
548 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700549 if (account == null) {
550 return;
551 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700552 if (!account.equals(mAccount)) {
553 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700554 mCachedSettings = mAccount.settings;
555 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700556 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800557 }
558
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700559 private void initFromSpinner(Bundle bundle, int action) {
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700560 String accountString = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700561 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700562 action = COMPOSE;
563 }
564 mFromSpinner.asyncInitFromSpinner(action, mAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700565 if (bundle != null) {
566 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
567 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
568 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
569 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
570 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
571 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
572 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700573 }
574 if (mReplyFromAccount == null) {
575 if (mDraft != null) {
576 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
577 } else if (mRefMessage != null) {
578 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
579 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700580 }
581 if (mReplyFromAccount == null) {
582 mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
583 mAccount.name, true, false);
584 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700585
Mindy Pereira62de1b12012-04-06 12:17:56 -0700586 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700587
Mindy Pereira62de1b12012-04-06 12:17:56 -0700588 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700589 // If there is only 1 account, just show that account.
590 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700591 // send mail from / save drafts to.
592 mFromStatic.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700593 mFromStaticText.setText(mAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700594 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700595 } else {
596 mFromStatic.setVisibility(View.VISIBLE);
597 mFromStaticText.setText(mAccount.name);
598 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700599 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800600 }
601
Mindy Pereira62de1b12012-04-06 12:17:56 -0700602 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
603 if (refMessage.accountUri != null) {
604 // This must be from combined inbox.
605 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
606 for (ReplyFromAccount from : replyFromAccounts) {
607 if (from.account.uri.equals(refMessage.accountUri)) {
608 return from;
609 }
610 }
611 return null;
612 } else {
613 return getReplyFromAccount(account, refMessage);
614 }
615 }
616
617 /**
618 * Given an account and which email address the message was sent to,
619 * return who the message should be sent from.
620 * @param account Account in which the message arrived.
621 * @param sentTo Email address to which the message was sent.
622 * @return the address from which to reply.
623 */
624 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
625 // First see if we are supposed to use the default address or
626 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -0700627 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700628 return getDefaultReplyFromAccount(account);
629 } else {
630 // If we aren't explicityly told which account to look for, look at
631 // all the message recipients and find one that matches
632 // a custom from or account.
633 List<String> allRecipients = new ArrayList<String>();
634 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.to)));
635 allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.cc)));
636 return getMatchingRecipient(account, allRecipients);
637 }
638 }
639
640 /**
641 * Compare all the recipients of an email to the current account and all
642 * custom addresses associated with that account. Return the match if there
643 * is one, or the default account if there isn't.
644 */
645 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
646 // Tokenize the list and place in a hashmap.
647 ReplyFromAccount matchingReplyFrom = null;
648 Rfc822Token[] tokens;
649 HashSet<String> recipientsMap = new HashSet<String>();
650 for (String address : sentTo) {
651 tokens = Rfc822Tokenizer.tokenize(address);
652 for (int i = 0; i < tokens.length; i++) {
653 recipientsMap.add(tokens[i].getAddress());
654 }
655 }
656
657 int matchingAddressCount = 0;
658 List<ReplyFromAccount> customFroms;
659 try {
660 customFroms = FromAddressSpinner.getAccountSpecificFroms(account);
661 if (customFroms != null) {
662 for (ReplyFromAccount entry : customFroms) {
663 if (recipientsMap.contains(entry.address)) {
664 matchingReplyFrom = entry;
665 matchingAddressCount++;
666 }
667 }
668 }
669 } catch (JSONException e) {
670 LogUtils.wtf(LOG_TAG, "Exception parsing from addresses for account %s",
671 account.name);
672 }
673 if (matchingAddressCount > 1) {
674 matchingReplyFrom = getDefaultReplyFromAccount(account);
675 }
676 return matchingReplyFrom;
677 }
678
679 private ReplyFromAccount getDefaultReplyFromAccount(Account account) {
680 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
681 for (ReplyFromAccount from : replyFromAccounts) {
682 if (from.isDefault) {
683 return from;
684 }
685 }
686 return new ReplyFromAccount(account, account.uri, account.name, account.name, true, false);
687 }
688
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700689 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
690 String sender = msg.from;
Mindy Pereira62de1b12012-04-06 12:17:56 -0700691 ReplyFromAccount replyFromAccount = null;
692 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
693 if (TextUtils.equals(account.name, sender)) {
694 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
695 mAccount.name, true, false);
696 } else {
697 for (ReplyFromAccount fromAccount : replyFromAccounts) {
698 if (TextUtils.equals(fromAccount.name, sender)) {
699 replyFromAccount = fromAccount;
700 break;
701 }
702 }
703 }
704 return replyFromAccount;
705 }
706
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800707 private void findViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -0800708 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800709 if (mCcBccButton != null) {
710 mCcBccButton.setOnClickListener(this);
711 }
712 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800713 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereira1f936682012-03-02 11:30:33 -0800714 mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
715 if (mAttachmentsButton != null) {
716 mAttachmentsButton.setOnClickListener(this);
717 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800718 mTo = (RecipientEditTextView) findViewById(R.id.to);
719 mCc = (RecipientEditTextView) findViewById(R.id.cc);
720 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Mindy Pereira82cc5662012-01-09 17:29:30 -0800721 // TODO: add special chips text change watchers before adding
722 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -0800723 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800724 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
725 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -0700726 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800727 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -0800728 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800729 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800730 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -0800731 }
732
Mindy Pereira75f66632012-01-11 11:42:02 -0800733 // Now that the message has been initialized from any existing draft or
734 // ref message data, set up listeners for any changes that occur to the
735 // message.
736 private void initChangeListeners() {
737 mSubject.addTextChangedListener(this);
738 mBodyView.addTextChangedListener(this);
739 mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
740 mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
741 mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
742 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -0800743 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -0800744 }
745
Mindy Pereira326c6602012-01-04 15:32:42 -0800746 private void initActionBar(int action) {
747 mComposeMode = action;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800748 ActionBar actionBar = getActionBar();
Mindy Pereira326c6602012-01-04 15:32:42 -0800749 if (action == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800750 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
751 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -0800752 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800753 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800754 if (mComposeModeAdapter == null) {
755 mComposeModeAdapter = new ComposeModeAdapter(this);
756 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800757 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
758 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Mindy Pereira326c6602012-01-04 15:32:42 -0800759 switch (action) {
760 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800761 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -0800762 break;
763 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800764 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -0800765 break;
766 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800767 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -0800768 break;
769 }
770 }
Mindy Pereirafbe40192012-03-20 10:40:45 -0700771 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
772 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
773 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -0800774 }
775
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800776 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700777 setSubject(mRefMessage, action);
778 // Setup recipients
779 if (action == FORWARD) {
780 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -0800781 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700782 initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700783 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700784 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
785 initAttachments(mRefMessage);
786 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800787 }
788
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700789 private void initFromDraftMessage(Message message) {
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700790 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
791
792 mDraft = message;
793 mDraftId = message.id;
794 mSubject.setText(message.subject);
795 mForward = message.draftType == UIProvider.DraftType.FORWARD;
796 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
797 addToAddresses(toAddresses);
798 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
799 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -0700800 if (message.hasAttachments) {
801 List<Attachment> attachments = message.getAttachments();
802 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -0700803 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -0700804 }
805 }
Mindy Pereira002ff522012-05-30 10:31:26 -0700806 int quotedTextIndex = message.appendRefMessageContent && !mForward ?
807 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700808 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -0700809 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700810 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira002ff522012-05-30 10:31:26 -0700811 CharSequence htmlText = Html.fromHtml(message.bodyHtml);
812 if (quotedTextIndex > -1) {
813 htmlText = htmlText.subSequence(0, quotedTextIndex);
814 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
815 message.bodyHtml.length());
816 }
817 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700818 } else {
Mindy Pereira002ff522012-05-30 10:31:26 -0700819 CharSequence bodyText = quotedTextIndex > -1 ?
820 message.bodyText.substring(0, quotedTextIndex) : message.bodyText;
821 if (quotedTextIndex > -1) {
822 quotedText = message.bodyText.substring(quotedTextIndex);
823 }
824 mBodyView.setText(bodyText);
825 }
826 if (quotedTextIndex > -1 && quotedText != null) {
827 mQuotedTextView.setQuotedTextFromDraft(quotedText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700828 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700829 }
830
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700831 /**
832 * Fill all the widgets with the content found in the Intent Extra, if any.
833 * Also apply the same style to all widgets. Note: if initFromExtras is
834 * called as a result of switching between reply, reply all, and forward per
835 * the latest revision of Gmail, and the user has already made changes to
836 * attachments on a previous incarnation of the message (as a reply, reply
837 * all, or forward), the original attachments from the message will not be
838 * re-instantiated. The user's changes will be respected. This follows the
839 * web gmail interaction.
840 */
841 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700842 // If we were invoked with a SENDTO intent, the value
843 // should take precedence
844 final Uri dataUri = intent.getData();
845 if (dataUri != null) {
846 if (MAIL_TO.equals(dataUri.getScheme())) {
847 initFromMailTo(dataUri.toString());
848 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -0700849 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700850 String toText = dataUri.getSchemeSpecificPart();
851 if (toText != null) {
852 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -0700853 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700854 }
855 }
856 }
857 }
858
859 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
860 if (extraStrings != null) {
861 addToAddresses(Arrays.asList(extraStrings));
862 }
863 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
864 if (extraStrings != null) {
865 addCcAddresses(Arrays.asList(extraStrings), null);
866 }
867 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
868 if (extraStrings != null) {
869 addBccAddresses(Arrays.asList(extraStrings));
870 }
871
872 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
873 if (extraString != null) {
874 mSubject.setText(extraString);
875 }
876
877 for (String extra : ALL_EXTRAS) {
878 if (intent.hasExtra(extra)) {
879 String value = intent.getStringExtra(extra);
880 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700881 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700882 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700883 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700884 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -0700885 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700886 } else if (EXTRA_SUBJECT.equals(extra)) {
887 mSubject.setText(value);
888 } else if (EXTRA_BODY.equals(extra)) {
889 setBody(value, true /* with signature */);
890 }
891 }
892 }
893
894 Bundle extras = intent.getExtras();
895 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700896 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
897 if (text != null) {
898 setBody(text, true /* with signature */);
899 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700900 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700901 }
902
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700903 @VisibleForTesting
904 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
905 // TODO: handle the case where there are spaces in the display name as well as the email
906 // such as "Guy with spaces <guy+with+spaces@gmail.com>" as they it could be encoded
907 // ambiguously.
908
909 // Since URLDecode.decode changes + into ' ', and + is a valid
910 // email character, we need to find/ replace these ourselves before
911 // decoding.
912 String replacePlus = s.replace("+", "%2B");
913 return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
914 }
915
916 /**
917 * Initialize the compose view from a String representing a mailTo uri.
918 * @param mailToString The uri as a string.
919 */
920 public void initFromMailTo(String mailToString) {
921 // We need to disguise this string as a URI in order to parse it
922 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
923 Uri uri = Uri.parse("foo://" + mailToString);
924 int index = mailToString.indexOf("?");
925 int length = "mailto".length() + 1;
926 String to;
927 try {
928 // Extract the recipient after mailto:
929 if (index == -1) {
930 to = decodeEmailInUri(mailToString.substring(length));
931 } else {
932 to = decodeEmailInUri(mailToString.substring(length, index));
933 }
Mindy Pereiradbe89962012-04-13 09:42:38 -0700934 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700935 } catch (UnsupportedEncodingException e) {
936 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
937 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
938 } else {
939 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
940 }
941 }
942
943 List<String> cc = uri.getQueryParameters("cc");
944 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
945
946 List<String> otherTo = uri.getQueryParameters("to");
947 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
948
949 List<String> bcc = uri.getQueryParameters("bcc");
950 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
951
952 List<String> subject = uri.getQueryParameters("subject");
953 if (subject.size() > 0) {
954 try {
955 mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
956 } catch (UnsupportedEncodingException e) {
957 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
958 e.getMessage(), subject);
959 }
960 }
961
962 List<String> body = uri.getQueryParameters("body");
963 if (body.size() > 0) {
964 try {
965 setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
966 true /* with signature */);
967 } catch (UnsupportedEncodingException e) {
968 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
969 }
970 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700971 }
972
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800973 private void initAttachments(Message refMessage) {
Mindy Pereira7a07fb42012-01-11 10:32:48 -0800974 mAttachmentsView.addAttachments(mAccount, refMessage);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800975 }
976
Paul Westbrookf97588b2012-03-20 11:11:37 -0700977 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -0700978 Bundle extras = intent.getExtras();
979 if (extras == null) {
980 extras = Bundle.EMPTY;
981 }
Paul Westbrookf97588b2012-03-20 11:11:37 -0700982 final String action = intent.getAction();
983 if (!mAttachmentsChanged) {
984 long totalSize = 0;
985 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
986 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
987 for (String uriString : uris) {
988 final Uri uri = Uri.parse(uriString);
989 long size = 0;
990 try {
Andy Huang5c5fd572012-04-08 18:19:29 -0700991 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -0700992 } catch (AttachmentFailureException e) {
993 // A toast has already been shown to the user,
994 // just break out of the loop.
995 LogUtils.e(LOG_TAG, e, "Error adding attachment");
996 }
997 totalSize += size;
998 }
999 }
1000 if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
1001 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1002 long size = 0;
1003 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001004 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001005 } catch (AttachmentFailureException e) {
1006 // A toast has already been shown to the user, so just
1007 // exit.
1008 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1009 }
1010 totalSize += size;
1011 }
1012
1013 if (Intent.ACTION_SEND_MULTIPLE.equals(action)
1014 && extras.containsKey(Intent.EXTRA_STREAM)) {
1015 ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
1016 for (Parcelable uri : uris) {
1017 long size = 0;
1018 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001019 size = mAttachmentsView.addAttachment(mAccount, (Uri) uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001020 } catch (AttachmentFailureException e) {
1021 // A toast has already been shown to the user,
1022 // just break out of the loop.
1023 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1024 }
1025 totalSize += size;
1026 }
1027 }
1028
1029 if (totalSize > 0) {
1030 mAttachmentsChanged = true;
1031 updateSaveUi();
1032 }
1033 }
1034 }
1035
1036
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001037 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1038 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001039 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1040 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001041 }
1042
1043 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001044 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001045 boolean ccVisible = mCcBccView.isCcVisible();
1046 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001047 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001048 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001049 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001050 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001051 : R.string.add_bcc_label));
1052 } else {
1053 mCcBccButton.setVisibility(View.GONE);
1054 }
1055 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001056 }
1057
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001058 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001059 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1060 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1061 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1062 if (showCc || showBcc) {
1063 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001064 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001065 }
1066 }
1067
Mindy Pereira326689d2012-05-17 10:14:14 -07001068 private void showCcBcc(Message refMessage) {
1069 if (refMessage != null) {
1070 boolean showCc = !TextUtils.isEmpty(refMessage.cc);
1071 boolean showBcc = !TextUtils.isEmpty(refMessage.bcc);
1072 if (showCc || showBcc) {
1073 mCcBccView.show(false, showCc, showBcc);
1074 }
1075 }
1076 updateHideOrShowCcBcc();
1077 }
1078
Mindy Pereira013194c2012-01-06 15:09:33 -08001079 /**
1080 * Add attachment and update the compose area appropriately.
1081 * @param data
1082 */
1083 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001084 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1085 }
1086
Andy Huang5c5fd572012-04-08 18:19:29 -07001087 public void addAttachmentAndUpdateView(Uri contentUri) {
1088 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001089 return;
1090 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001091 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001092 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1093 } catch (AttachmentFailureException e) {
1094 // A toast has already been shown to the user, no need to do
1095 // anything.
1096 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1097 }
1098 }
1099
1100 public void addAttachmentAndUpdateView(Attachment attachment) {
1101 try {
1102 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001103 if (size > 0) {
1104 mAttachmentsChanged = true;
1105 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001106 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001107 } catch (AttachmentFailureException e) {
1108 // A toast has already been shown to the user, no need to do
1109 // anything.
1110 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mindy Pereira013194c2012-01-06 15:09:33 -08001111 }
1112 }
1113
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001114 void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001115 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001116 // Don't populate the address if this is a forward.
1117 if (action == ComposeActivity.FORWARD) {
1118 return;
1119 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001120 initReplyRecipients(mAccount.name, refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001121 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001122
Mindy Pereira818143e2012-01-11 13:59:49 -08001123 @VisibleForTesting
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001124 void initReplyRecipients(String account, Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001125 // This is the email address of the current user, i.e. the one composing
1126 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001127 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001128 String fromAddress = refMessage.from;
1129 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
1130 String replytoAddress = refMessage.replyTo;
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001131 final Collection<String> toAddresses;
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001132
1133 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1134 // Cc list is the union of the To and Cc recipients of the original
1135 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001136 // already on the To list.
1137 if (action == ComposeActivity.REPLY) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001138 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1139 new String[0]);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001140 addToAddresses(toAddresses);
1141 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001142 final Set<String> ccAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001143 toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1144 new String[0]);
Mindy Pereira154386a2012-01-11 13:02:33 -08001145 addToAddresses(toAddresses);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001146 addRecipients(accountEmail, ccAddresses, sentToAddresses);
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001147 addRecipients(accountEmail, ccAddresses,
1148 Utils.splitCommaSeparatedString(refMessage.cc));
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001149 addCcAddresses(ccAddresses, toAddresses);
1150 }
1151 }
1152
1153 private void addToAddresses(Collection<String> addresses) {
1154 addAddressesToList(addresses, mTo);
1155 }
1156
1157 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001158 addCcAddressesToList(tokenizeAddressList(addresses),
1159 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001160 }
1161
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001162 private void addBccAddresses(Collection<String> addresses) {
1163 addAddressesToList(addresses, mBcc);
1164 }
1165
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001166 @VisibleForTesting
1167 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1168 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1169 String address;
1170
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001171 if (compareToList == null) {
1172 for (Rfc822Token[] tokens : addresses) {
1173 for (int i = 0; i < tokens.length; i++) {
1174 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001175 list.append(address + END_TOKEN);
1176 }
1177 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001178 } else {
1179 HashSet<String> compareTo = convertToHashSet(compareToList);
1180 for (Rfc822Token[] tokens : addresses) {
1181 for (int i = 0; i < tokens.length; i++) {
1182 address = tokens[i].toString();
1183 // Check if this is a duplicate:
1184 if (!compareTo.contains(tokens[i].getAddress())) {
1185 // Get the address here
1186 list.append(address + END_TOKEN);
1187 }
1188 }
1189 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001190 }
1191 }
1192
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001193 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
1194 HashSet<String> hash = new HashSet<String>();
1195 for (Rfc822Token[] tokens : list) {
1196 for (int i = 0; i < tokens.length; i++) {
1197 hash.add(tokens[i].getAddress());
1198 }
1199 }
1200 return hash;
1201 }
1202
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001203 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1204 @VisibleForTesting
1205 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1206
1207 for (String address: addresses) {
1208 tokenized.add(Rfc822Tokenizer.tokenize(address));
1209 }
1210 return tokenized;
1211 }
1212
1213 @VisibleForTesting
1214 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1215 for (String address : addresses) {
1216 addAddressToList(address, list);
1217 }
1218 }
1219
1220 private void addAddressToList(String address, RecipientEditTextView list) {
1221 if (address == null || list == null)
1222 return;
1223
1224 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1225
1226 for (int i = 0; i < tokens.length; i++) {
1227 list.append(tokens[i] + END_TOKEN);
1228 }
1229 }
1230
1231 @VisibleForTesting
1232 protected Collection<String> initToRecipients(String account, String accountEmail,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001233 String senderAddress, String replyToAddress, String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001234 // The To recipient is the reply-to address specified in the original
1235 // message, unless it is:
1236 // the current user OR a custom from of the current user, in which case
1237 // it's the To recipient list of the original message.
1238 // OR missing, in which case use the sender of the original message
1239 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001240 if (!TextUtils.isEmpty(replyToAddress)) {
1241 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001242 } else {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001243 toAddresses.add(senderAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001244 }
1245 return toAddresses;
1246 }
1247
1248 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
1249 for (String email : addresses) {
1250 // Do not add this account, or any of the custom froms, to the list
1251 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001252 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001253 if (!account.equalsIgnoreCase(recipientAddress)) {
1254 recipients.add(email.replace("\"\"", ""));
1255 }
1256 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001257 }
1258
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001259 private void setSubject(Message refMessage, int action) {
1260 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001261 String prefix;
1262 String correctedSubject = null;
1263 if (action == ComposeActivity.COMPOSE) {
1264 prefix = "";
1265 } else if (action == ComposeActivity.FORWARD) {
1266 prefix = getString(R.string.forward_subject_label);
1267 } else {
1268 prefix = getString(R.string.reply_subject_label);
1269 }
1270
1271 // Don't duplicate the prefix
1272 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
1273 correctedSubject = subject;
1274 } else {
1275 correctedSubject = String
1276 .format(getString(R.string.formatted_subject), prefix, subject);
1277 }
1278 mSubject.setText(correctedSubject);
1279 }
1280
Mindy Pereira818143e2012-01-11 13:59:49 -08001281 private void initRecipients() {
1282 setupRecipients(mTo);
1283 setupRecipients(mCc);
1284 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001285 }
1286
Mindy Pereira818143e2012-01-11 13:59:49 -08001287 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001288 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereirac17d0732011-12-29 10:46:19 -08001289 view.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001290 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001291 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001292 int offset = accountName.indexOf("@") + 1;
1293 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001294 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001295 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001296 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001297 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001298 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001299 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001300 }
1301
1302 @Override
1303 public void onClick(View v) {
1304 int id = v.getId();
1305 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001306 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001307 // Verify that cc/ bcc aren't showing.
1308 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001309 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001310 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001311 case R.id.add_attachment:
1312 doAttach();
1313 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001314 }
1315 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001316
1317 @Override
1318 public boolean onCreateOptionsMenu(Menu menu) {
1319 super.onCreateOptionsMenu(menu);
1320 MenuInflater inflater = getMenuInflater();
1321 inflater.inflate(R.menu.compose_menu, menu);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001322 mSave = menu.findItem(R.id.save);
1323 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001324 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1325 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1326 if (helpItem != null) {
1327 helpItem.setVisible(mAccount != null
1328 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1329 }
1330 if (sendFeedbackItem != null) {
1331 sendFeedbackItem.setVisible(mAccount != null
1332 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1333 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001334 return true;
1335 }
1336
1337 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001338 public boolean onPrepareOptionsMenu(Menu menu) {
1339 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001340 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001341 // Its possible there is a menu item OR a button.
1342 boolean ccFieldVisible = mCc.isShown();
1343 boolean bccFieldVisible = mBcc.isShown();
1344 if (!ccFieldVisible || !bccFieldVisible) {
1345 ccBcc.setVisible(true);
1346 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1347 : R.string.add_bcc_label));
1348 } else {
1349 ccBcc.setVisible(false);
1350 }
1351 }
Mindy Pereira75f66632012-01-11 11:42:02 -08001352 if (mSave != null) {
1353 mSave.setEnabled(shouldSave());
1354 }
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001355 return true;
1356 }
1357
1358 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001359 public boolean onOptionsItemSelected(MenuItem item) {
1360 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001361 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001362 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -08001363 case R.id.add_attachment:
Mindy Pereira013194c2012-01-06 15:09:33 -08001364 doAttach();
Mindy Pereira7b56a612011-12-14 12:32:28 -08001365 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001366 case R.id.add_cc_bcc:
1367 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001368 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001369 case R.id.save:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001370 doSave(true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001371 break;
1372 case R.id.send:
1373 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001374 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001375 case R.id.discard:
1376 doDiscard();
1377 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001378 case R.id.settings:
1379 Utils.showSettings(this, mAccount);
1380 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001381 case android.R.id.home:
1382 finish();
1383 break;
1384 case R.id.help_info_menu_item:
1385 // TODO: enable context sensitive help
Paul Westbrook498e76d2012-04-12 16:33:02 -07001386 Utils.showHelp(this, mAccount, null);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001387 break;
1388 case R.id.feedback_menu_item:
1389 Utils.sendFeedback(this, mAccount);
1390 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001391 default:
1392 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001393 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001394 }
1395 return !handled ? super.onOptionsItemSelected(item) : handled;
1396 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001397
Mindy Pereira33fe9082012-01-09 16:24:30 -08001398 private void doSend() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001399 sendOrSaveWithSanityChecks(false, true, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001400 }
1401
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001402 private void doSave(boolean showToast, boolean resetIME) {
1403 sendOrSaveWithSanityChecks(true, showToast, false);
1404 if (resetIME) {
1405 // Clear the IME composing suggestions from the body.
1406 BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1407 }
Mindy Pereira33fe9082012-01-09 16:24:30 -08001408 }
1409
Mindy Pereira82cc5662012-01-09 17:29:30 -08001410 /*package*/ interface SendOrSaveCallback {
1411 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001412 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1413 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001414 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1415 }
1416
1417 /*package*/ static class SendOrSaveTask implements Runnable {
1418 private final Context mContext;
1419 private final SendOrSaveCallback mSendOrSaveCallback;
1420 @VisibleForTesting
1421 final SendOrSaveMessage mSendOrSaveMessage;
1422
1423 public SendOrSaveTask(Context context, SendOrSaveMessage message,
1424 SendOrSaveCallback callback) {
1425 mContext = context;
1426 mSendOrSaveCallback = callback;
1427 mSendOrSaveMessage = message;
1428 }
1429
1430 @Override
1431 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001432 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001433
Mindy Pereira92551d02012-04-05 11:31:12 -07001434 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001435 Message message = mSendOrSaveCallback.getMessage();
1436 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001437 // If a previous draft has been saved, in an account that is different
1438 // than what the user wants to send from, remove the old draft, and treat this
1439 // as a new message
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001440 if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001441 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1442 ContentResolver resolver = mContext.getContentResolver();
1443 ContentValues values = new ContentValues();
1444 values.put(BaseColumns._ID, messageId);
Mindy Pereira92551d02012-04-05 11:31:12 -07001445 if (selectedAccount.account.expungeMessageUri != null) {
1446 resolver.update(selectedAccount.account.expungeMessageUri, values, null,
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001447 null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001448 } else {
1449 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001450 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001451 // reset messageId to 0, so a new message will be created
1452 messageId = UIProvider.INVALID_MESSAGE_ID;
1453 }
1454 }
1455
1456 final long messageIdToSave = messageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001457 if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001458 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001459 mContext.getContentResolver().update(
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001460 Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1461 sendOrSaveMessage.mValues, null, null);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001462 } else {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001463 ContentResolver resolver = mContext.getContentResolver();
Mindy Pereira92551d02012-04-05 11:31:12 -07001464 Uri messageUri = resolver
1465 .insert(sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
1466 : selectedAccount.account.sendMessageUri,
1467 sendOrSaveMessage.mValues);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001468 if (sendOrSaveMessage.mSave && messageUri != null) {
1469 Cursor messageCursor = resolver.query(messageUri,
1470 UIProvider.MESSAGE_PROJECTION, null, null, null);
Paul Westbrookba558482012-03-19 11:00:24 -07001471 if (messageCursor != null) {
1472 try {
1473 if (messageCursor.moveToFirst()) {
1474 // Broadcast notification that a new message has
1475 // been allocated
1476 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1477 new Message(messageCursor));
1478 }
1479 } finally {
1480 messageCursor.close();
1481 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001482 }
1483 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001484 }
1485
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001486 if (!sendOrSaveMessage.mSave) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001487 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001488 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001489 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001490 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001491 UIProvider.incrementRecipientsTimesContacted(mContext,
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001492 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001493 }
1494 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1495 }
1496 }
1497
1498 // Array of the outstanding send or save tasks. Access is synchronized
1499 // with the object itself
1500 /* package for testing */
1501 ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
1502 private int mRequestId;
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001503 private String mSignature;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001504
1505 /*package*/ static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07001506 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001507 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001508 final String mRefMessageId;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001509 final boolean mSave;
1510 final int mRequestId;
1511
Mindy Pereira92551d02012-04-05 11:31:12 -07001512 public SendOrSaveMessage(ReplyFromAccount account, ContentValues values,
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001513 String refMessageId, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001514 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001515 mValues = values;
1516 mRefMessageId = refMessageId;
1517 mSave = save;
1518 mRequestId = mValues.hashCode() ^ hashCode();
1519 }
1520
1521 int requestId() {
1522 return mRequestId;
1523 }
1524 }
1525
1526 /**
1527 * Get the to recipients.
1528 */
1529 public String[] getToAddresses() {
1530 return getAddressesFromList(mTo);
1531 }
1532
1533 /**
1534 * Get the cc recipients.
1535 */
1536 public String[] getCcAddresses() {
1537 return getAddressesFromList(mCc);
1538 }
1539
1540 /**
1541 * Get the bcc recipients.
1542 */
1543 public String[] getBccAddresses() {
1544 return getAddressesFromList(mBcc);
1545 }
1546
1547 public String[] getAddressesFromList(RecipientEditTextView list) {
1548 if (list == null) {
1549 return new String[0];
1550 }
1551 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1552 int count = tokens.length;
1553 String[] result = new String[count];
1554 for (int i = 0; i < count; i++) {
1555 result[i] = tokens[i].toString();
1556 }
1557 return result;
1558 }
1559
1560 /**
1561 * Check for invalid email addresses.
1562 * @param to String array of email addresses to check.
1563 * @param wrongEmailsOut Emails addresses that were invalid.
1564 */
1565 public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
1566 for (String email : to) {
1567 if (!mValidator.isValid(email)) {
1568 wrongEmailsOut.add(email);
1569 }
1570 }
1571 }
1572
1573 /**
1574 * Show an error because the user has entered an invalid recipient.
1575 * @param message
1576 */
1577 public void showRecipientErrorDialog(String message) {
1578 // Only 1 invalid recipients error dialog should be allowed up at a
1579 // time.
1580 if (mRecipientErrorDialog != null) {
1581 mRecipientErrorDialog.dismiss();
1582 }
1583 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1584 R.string.recipient_error_dialog_title)
1585 .setIconAttribute(android.R.attr.alertDialogIcon)
1586 .setCancelable(false)
1587 .setPositiveButton(
1588 R.string.ok, new Dialog.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001589 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08001590 public void onClick(DialogInterface dialog, int which) {
1591 // after the user dismisses the recipient error
1592 // dialog we want to make sure to refocus the
1593 // recipient to field so they can fix the issue
1594 // easily
1595 if (mTo != null) {
1596 mTo.requestFocus();
1597 }
1598 mRecipientErrorDialog = null;
1599 }
1600 }).show();
1601 }
1602
1603 /**
1604 * Update the state of the UI based on whether or not the current draft
1605 * needs to be saved and the message is not empty.
1606 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001607 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001608 if (mSave != null) {
1609 mSave.setEnabled((shouldSave() && !isBlank()));
1610 }
1611 }
1612
1613 /**
1614 * Returns true if we need to save the current draft.
1615 */
1616 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001617 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001618 // The message should only be saved if:
1619 // It hasn't been sent AND
1620 // Some text has been added to the message OR
1621 // an attachment has been added or removed
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001622 return (mTextChanged || mAttachmentsChanged ||
Mindy Pereira82cc5662012-01-09 17:29:30 -08001623 (mReplyFromChanged && !isBlank()));
1624 }
1625 }
1626
1627 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001628 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08001629 * @return boolean
1630 */
1631 public boolean isBlank() {
1632 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08001633 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1634 mBodyView.getText().toString()) == 0)
1635 && mTo.length() == 0
1636 && mCc.length() == 0 && mBcc.length() == 0
1637 && mAttachmentsView.getAttachments().size() == 0;
1638 }
1639
1640 @VisibleForTesting
1641 protected int getSignatureStartPosition(String signature, String bodyText) {
1642 int startPos = -1;
1643
1644 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1645 return startPos;
1646 }
1647
1648 int bodyLength = bodyText.length();
1649 int signatureLength = signature.length();
1650 String printableVersion = convertToPrintableSignature(signature);
1651 int printableLength = printableVersion.length();
1652
1653 if (bodyLength >= printableLength
1654 && bodyText.substring(bodyLength - printableLength)
1655 .equals(printableVersion)) {
1656 startPos = bodyLength - printableLength;
1657 } else if (bodyLength >= signatureLength
1658 && bodyText.substring(bodyLength - signatureLength)
1659 .equals(signature)) {
1660 startPos = bodyLength - signatureLength;
1661 }
1662 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001663 }
1664
1665 /**
1666 * Allows any changes made by the user to be ignored. Called when the user
1667 * decides to discard a draft.
1668 */
1669 private void discardChanges() {
1670 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001671 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001672 mReplyFromChanged = false;
1673 }
1674
1675 /**
Mindy Pereira181df782012-03-01 13:32:44 -08001676 * @param body
1677 * @param save
1678 * @param showToast
1679 * @return Whether the send or save succeeded.
1680 */
1681 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1682 final boolean orientationChanged) {
1683 String[] to, cc, bcc;
1684 Editable body = mBodyView.getEditableText();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001685
Mindy Pereira181df782012-03-01 13:32:44 -08001686 if (orientationChanged) {
1687 to = cc = bcc = new String[0];
1688 } else {
1689 to = getToAddresses();
1690 cc = getCcAddresses();
1691 bcc = getBccAddresses();
1692 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001693
Mindy Pereira181df782012-03-01 13:32:44 -08001694 // Don't let the user send to nobody (but it's okay to save a message
1695 // with no recipients)
1696 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1697 showRecipientErrorDialog(getString(R.string.recipient_needed));
1698 return false;
1699 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001700
Mindy Pereira181df782012-03-01 13:32:44 -08001701 List<String> wrongEmails = new ArrayList<String>();
1702 if (!save) {
1703 checkInvalidEmails(to, wrongEmails);
1704 checkInvalidEmails(cc, wrongEmails);
1705 checkInvalidEmails(bcc, wrongEmails);
1706 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001707
Mindy Pereira181df782012-03-01 13:32:44 -08001708 // Don't let the user send an email with invalid recipients
1709 if (wrongEmails.size() > 0) {
1710 String errorText = String.format(getString(R.string.invalid_recipient),
1711 wrongEmails.get(0));
1712 showRecipientErrorDialog(errorText);
1713 return false;
1714 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001715
Mindy Pereira181df782012-03-01 13:32:44 -08001716 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07001717 @Override
Mindy Pereira181df782012-03-01 13:32:44 -08001718 public void onClick(DialogInterface dialog, int which) {
1719 sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1720 }
1721 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001722
Mindy Pereira181df782012-03-01 13:32:44 -08001723 // Show a warning before sending only if there are no attachments.
1724 if (!save) {
1725 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1726 boolean warnAboutEmptySubject = isSubjectEmpty();
1727 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001728
Mindy Pereira181df782012-03-01 13:32:44 -08001729 // A warning about an empty body may not be warranted when
1730 // forwarding mails, since a common use case is to forward
1731 // quoted text and not append any more text.
1732 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001733
Mindy Pereira181df782012-03-01 13:32:44 -08001734 // When we bring up a dialog warning the user about a send,
1735 // assume that they accept sending the message. If they do not,
1736 // the dialog listener is required to enable sending again.
1737 if (warnAboutEmptySubject) {
1738 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1739 return true;
1740 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001741
Mindy Pereira181df782012-03-01 13:32:44 -08001742 if (warnAboutEmptyBody) {
1743 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1744 return true;
1745 }
1746 }
1747 // Ask for confirmation to send (if always required)
1748 if (showSendConfirmation()) {
1749 showSendConfirmDialog(R.string.confirm_send_message, listener);
1750 return true;
1751 }
1752 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001753
Mindy Pereira181df782012-03-01 13:32:44 -08001754 sendOrSave(body, save, showToast, false);
1755 return true;
1756 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001757
Mindy Pereira181df782012-03-01 13:32:44 -08001758 /**
1759 * Returns a boolean indicating whether warnings should be shown for empty
1760 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07001761 *
Mindy Pereira181df782012-03-01 13:32:44 -08001762 * @return True if a warning should be shown for empty text fields
1763 */
1764 protected boolean showEmptyTextWarnings() {
1765 return mAttachmentsView.getAttachments().size() == 0;
1766 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001767
Mindy Pereira181df782012-03-01 13:32:44 -08001768 /**
1769 * Returns a boolean indicating whether the user should confirm each send
1770 *
1771 * @return True if a warning should be on each send
1772 */
1773 protected boolean showSendConfirmation() {
1774 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1775 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001776
Mindy Pereira181df782012-03-01 13:32:44 -08001777 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1778 if (mSendConfirmDialog != null) {
1779 mSendConfirmDialog.dismiss();
1780 mSendConfirmDialog = null;
1781 }
1782 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1783 .setTitle(R.string.confirm_send_title)
1784 .setIconAttribute(android.R.attr.alertDialogIcon)
1785 .setPositiveButton(R.string.send, listener)
1786 .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1787 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001788
Mindy Pereira181df782012-03-01 13:32:44 -08001789 /**
1790 * Returns whether the ComposeArea believes there is any text in the body of
1791 * the composition. TODO: When ComposeArea controls the Body as well, add
1792 * that here.
1793 */
1794 public boolean isBodyEmpty() {
1795 return !mQuotedTextView.isTextIncluded();
1796 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001797
Mindy Pereira181df782012-03-01 13:32:44 -08001798 /**
1799 * Test to see if the subject is empty.
1800 *
1801 * @return boolean.
1802 */
1803 // TODO: this will likely go away when composeArea.focus() is implemented
1804 // after all the widget control is moved over.
1805 public boolean isSubjectEmpty() {
1806 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1807 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001808
Mindy Pereira181df782012-03-01 13:32:44 -08001809 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001810 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07001811 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
1812 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode) {
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001813 ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001814
Mindy Pereirac2031972012-04-03 09:38:35 -07001815 String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
1816
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001817 MessageModification.putToAddresses(values, message.getToAddresses());
1818 MessageModification.putCcAddresses(values, message.getCcAddresses());
1819 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001820
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001821 MessageModification.putCustomFromAddress(values, message.from);
Mindy Pereira92551d02012-04-05 11:31:12 -07001822
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001823 MessageModification.putSubject(values, message.subject);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001824 String htmlBody = Html.toHtml(body);
Paul Westbrook05b92b82012-04-20 13:29:37 -07001825
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001826 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1827 StringBuilder fullBody = new StringBuilder(htmlBody);
1828 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001829 // HTML gets converted to text for now
1830 final String text = quotedText.toString();
1831 if (QuotedTextView.containsQuotedText(text)) {
1832 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07001833 final int quoteStartPos = fullBody.length() + pos;
1834 fullBody.append(text);
1835 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07001836 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07001837 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001838 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07001839 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1840 // This shouldn't happen, but just use what we have,
1841 // and don't do server-side expansion
1842 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08001843 }
1844 }
Mindy Pereira002ff522012-05-30 10:31:26 -07001845 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07001846 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07001847 if (refMessage != null) {
1848 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1849 MessageModification.putBodyHtml(values, fullBody.toString());
1850 }
1851 if (!TextUtils.isEmpty(refMessage.bodyText)) {
1852 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1853 }
1854 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07001855 MessageModification.putBodyHtml(values, fullBody.toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07001856 MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1857 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001858 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07001859 if (!TextUtils.isEmpty(refMessageId)) {
1860 MessageModification.putRefMessageId(values, refMessageId);
1861 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001862
Mindy Pereira92551d02012-04-05 11:31:12 -07001863 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(replyFromAccount,
Mindy Pereira181df782012-03-01 13:32:44 -08001864 values, refMessageId, save);
1865 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001866
Mindy Pereira181df782012-03-01 13:32:44 -08001867 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001868
Mindy Pereira181df782012-03-01 13:32:44 -08001869 // Do the send/save action on the specified handler to avoid possible
1870 // ANRs
1871 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08001872
Mindy Pereira181df782012-03-01 13:32:44 -08001873 return sendOrSaveMessage.requestId();
1874 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001875
Mindy Pereira002ff522012-05-30 10:31:26 -07001876 private static int getDraftType(int mode) {
1877 int draftType = -1;
1878 switch (mode) {
1879 case ComposeActivity.COMPOSE:
1880 draftType = DraftType.COMPOSE;
1881 break;
1882 case ComposeActivity.REPLY:
1883 draftType = DraftType.REPLY;
1884 break;
1885 case ComposeActivity.REPLY_ALL:
1886 draftType = DraftType.REPLY_ALL;
1887 break;
1888 case ComposeActivity.FORWARD:
1889 draftType = DraftType.FORWARD;
1890 break;
1891 }
1892 return draftType;
1893 }
1894
Mindy Pereira181df782012-03-01 13:32:44 -08001895 private void sendOrSave(Spanned body, boolean save, boolean showToast,
1896 boolean orientationChanged) {
1897 // Check if user is a monkey. Monkeys can compose and hit send
1898 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07001899 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08001900 return;
1901 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001902
Mindy Pereira181df782012-03-01 13:32:44 -08001903 String[] to, cc, bcc;
1904 if (orientationChanged) {
1905 to = cc = bcc = new String[0];
1906 } else {
1907 to = getToAddresses();
1908 cc = getCcAddresses();
1909 bcc = getBccAddresses();
1910 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001911
Mindy Pereira181df782012-03-01 13:32:44 -08001912 SendOrSaveCallback callback = new SendOrSaveCallback() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001913 private int mRestoredRequestId;
1914
Marc Blank0bbc8582012-04-23 15:07:57 -07001915 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08001916 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08001917 synchronized (mActiveTasks) {
1918 int numTasks = mActiveTasks.size();
1919 if (numTasks == 0) {
1920 // Start service so we won't be killed if this app is
1921 // put in the background.
1922 startService(new Intent(ComposeActivity.this, EmptyService.class));
1923 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001924
Mindy Pereira181df782012-03-01 13:32:44 -08001925 mActiveTasks.add(sendOrSaveTask);
1926 }
1927 if (sTestSendOrSaveCallback != null) {
1928 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1929 }
1930 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001931
Marc Blank0bbc8582012-04-23 15:07:57 -07001932 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001933 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1934 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08001935 synchronized (mDraftLock) {
1936 mDraftId = message.id;
1937 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001938 if (sRequestMessageIdMap != null) {
1939 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1940 }
Mindy Pereira181df782012-03-01 13:32:44 -08001941 // Cache request message map, in case the process is killed
1942 saveRequestMap();
1943 }
1944 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001945 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08001946 }
1947 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001948
Marc Blank0bbc8582012-04-23 15:07:57 -07001949 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001950 public Message getMessage() {
1951 synchronized (mDraftLock) {
1952 return mDraft;
1953 }
1954 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001955
Marc Blank0bbc8582012-04-23 15:07:57 -07001956 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001957 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1958 if (success) {
1959 // Successfully sent or saved so reset change markers
1960 discardChanges();
1961 } else {
1962 // A failure happened with saving/sending the draft
1963 // TODO(pwestbro): add a better string that should be used
1964 // when failing to send or save
1965 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1966 .show();
1967 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001968
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001969 int numTasks;
1970 synchronized (mActiveTasks) {
1971 // Remove the task from the list of active tasks
1972 mActiveTasks.remove(task);
1973 numTasks = mActiveTasks.size();
1974 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001975
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001976 if (numTasks == 0) {
1977 // Stop service so we can be killed.
1978 stopService(new Intent(ComposeActivity.this, EmptyService.class));
1979 }
1980 if (sTestSendOrSaveCallback != null) {
1981 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1982 }
1983 }
Mindy Pereira181df782012-03-01 13:32:44 -08001984 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08001985
Mindy Pereira181df782012-03-01 13:32:44 -08001986 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07001987 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08001988 String fromAddress = selectedAccount.name;
1989 if (selectedAccount == null || fromAddress == null) {
1990 // We don't have either the selected account or from address,
1991 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07001992 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08001993 fromAddress = mAccount.name;
1994 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001995
Mindy Pereira181df782012-03-01 13:32:44 -08001996 if (mSendSaveTaskHandler == null) {
1997 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1998 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001999
Mindy Pereira181df782012-03-01 13:32:44 -08002000 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2001 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002002
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002003 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002004 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2005 mQuotedTextView.getQuotedTextIfIncluded(), callback,
Mindy Pereira12575862012-03-21 16:30:54 -07002006 mSendSaveTaskHandler, save, mComposeMode);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002007
Mindy Pereira181df782012-03-01 13:32:44 -08002008 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
2009 mRecipient = selectedAccount.name;
2010 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002011 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002012
Mindy Pereira181df782012-03-01 13:32:44 -08002013 // Don't display the toast if the user is just changing the orientation,
2014 // but we still need to save the draft to the cursor because this is how we restore
2015 // the attachments when the configuration change completes.
2016 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2017 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2018 Toast.LENGTH_LONG).show();
2019 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002020
Mindy Pereira181df782012-03-01 13:32:44 -08002021 // Need to update variables here because the send or save completes
2022 // asynchronously even though the toast shows right away.
2023 discardChanges();
2024 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002025
Mindy Pereira181df782012-03-01 13:32:44 -08002026 // If we are sending, finish the activity
2027 if (!save) {
2028 finish();
2029 }
2030 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002031
Mindy Pereira181df782012-03-01 13:32:44 -08002032 /**
2033 * Save the state of the request messageid map. This allows for the Gmail
2034 * process to be killed, but and still allow for ComposeActivity instances
2035 * to be recreated correctly.
2036 */
2037 private void saveRequestMap() {
2038 // TODO: store the request map in user preferences.
2039 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002040
Mindy Pereira013194c2012-01-06 15:09:33 -08002041 public void doAttach() {
2042 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2043 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002044 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Mindy Pereira181df782012-03-01 13:32:44 -08002045 if (android.provider.Settings.System.getInt(getContentResolver(),
2046 UIProvider.getAttachmentTypeSetting(), 0) != 0) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002047 i.setType("*/*");
2048 } else {
2049 i.setType("image/*");
2050 }
2051 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002052 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2053 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002054 }
2055
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002056 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002057 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002058 if (mCcBccButton != null) {
2059 mCcBccButton.setVisibility(View.GONE);
2060 }
2061 }
2062
Mindy Pereira326c6602012-01-04 15:32:42 -08002063 @Override
2064 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002065 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002066 if (position == ComposeActivity.REPLY) {
2067 mComposeMode = ComposeActivity.REPLY;
2068 } else if (position == ComposeActivity.REPLY_ALL) {
2069 mComposeMode = ComposeActivity.REPLY_ALL;
2070 } else if (position == ComposeActivity.FORWARD) {
2071 mComposeMode = ComposeActivity.FORWARD;
2072 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002073 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002074 resetMessageForModeChange();
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002075 if (mRefMessage != null) {
2076 initFromRefMessage(mComposeMode, mAccount.name);
2077 }
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002078 }
Mindy Pereira326c6602012-01-04 15:32:42 -08002079 return true;
2080 }
2081
Mindy Pereira154386a2012-01-11 13:02:33 -08002082 private void resetMessageForModeChange() {
2083 // When switching between reply, reply all, forward,
2084 // follow the behavior of webview.
2085 // The contents of the following fields are cleared
2086 // so that they can be populated directly from the
2087 // ref message:
2088 // 1) Any recipient fields
2089 // 2) The subject
2090 mTo.setText("");
2091 mCc.setText("");
2092 mBcc.setText("");
2093 // Any edits to the subject are replaced with the original subject.
2094 mSubject.setText("");
2095
2096 // Any changes to the contents of the following fields are kept:
2097 // 1) Body
2098 // 2) Attachments
2099 // If the user made changes to attachments, keep their changes.
2100 if (!mAttachmentsChanged) {
2101 mAttachmentsView.deleteAllAttachments();
2102 }
2103 }
2104
Mindy Pereira326c6602012-01-04 15:32:42 -08002105 private class ComposeModeAdapter extends ArrayAdapter<String> {
2106
2107 private LayoutInflater mInflater;
2108
2109 public ComposeModeAdapter(Context context) {
2110 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2111 .getStringArray(R.array.compose_modes));
2112 }
2113
2114 private LayoutInflater getInflater() {
2115 if (mInflater == null) {
2116 mInflater = LayoutInflater.from(getContext());
2117 }
2118 return mInflater;
2119 }
2120
2121 @Override
2122 public View getView(int position, View convertView, ViewGroup parent) {
2123 if (convertView == null) {
2124 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2125 }
2126 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2127 return super.getView(position, convertView, parent);
2128 }
2129 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002130
2131 @Override
2132 public void onRespondInline(String text) {
2133 appendToBody(text, false);
2134 }
2135
2136 /**
2137 * Append text to the body of the message. If there is no existing body
2138 * text, just sets the body to text.
2139 *
2140 * @param text
2141 * @param withSignature True to append a signature.
2142 */
2143 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002144 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002145 if (bodyText != null && bodyText.length() > 0) {
2146 bodyText.append(text);
2147 } else {
2148 setBody(text, withSignature);
2149 }
2150 }
2151
2152 /**
2153 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002154 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002155 * @param text
2156 * @param withSignature True to append a signature.
2157 */
2158 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002159 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002160 if (withSignature) {
2161 appendSignature();
2162 }
2163 }
2164
2165 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002166 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002167 boolean hasFocus = mBodyView.hasFocus();
Mindy Pereirab13917c2012-03-29 08:08:19 -07002168 if (!TextUtils.equals(newSignature, mSignature)) {
2169 mSignature = newSignature;
2170 if (!TextUtils.isEmpty(mSignature)
2171 && getSignatureStartPosition(mSignature,
2172 mBodyView.getText().toString()) < 0) {
2173 // Appending a signature does not count as changing text.
2174 mBodyView.removeTextChangedListener(this);
2175 mBodyView.append(convertToPrintableSignature(mSignature));
2176 mBodyView.addTextChangedListener(this);
2177 }
Mindy Pereira433b1982012-04-03 11:53:07 -07002178 if (hasFocus) {
2179 focusBody();
2180 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002181 }
2182 }
2183
2184 private String convertToPrintableSignature(String signature) {
2185 String signatureResource = getResources().getString(R.string.signature);
2186 if (signature == null) {
2187 signature = "";
2188 }
2189 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002190 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002191
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08002192 @Override
2193 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07002194 mReplyFromAccount = mFromSpinner.getCurrentAccount();
2195 if (!mAccount.equals(mReplyFromAccount.account)) {
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002196 setAccount(mReplyFromAccount.account);
2197
Mindy Pereira181df782012-03-01 13:32:44 -08002198 // TODO: handle discarding attachments when switching accounts.
2199 // Only enable save for this draft if there is any other content
2200 // in the message.
2201 if (!isBlank()) {
2202 enableSave(true);
2203 }
2204 mReplyFromChanged = true;
2205 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002206 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002207 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002208
2209 public void enableSave(boolean enabled) {
2210 if (mSave != null) {
2211 mSave.setEnabled(enabled);
2212 }
2213 }
2214
2215 public void enableSend(boolean enabled) {
2216 if (mSend != null) {
2217 mSend.setEnabled(enabled);
2218 }
2219 }
2220
2221 /**
2222 * Handles button clicks from any error dialogs dealing with sending
2223 * a message.
2224 */
2225 @Override
2226 public void onClick(DialogInterface dialog, int which) {
2227 switch (which) {
2228 case DialogInterface.BUTTON_POSITIVE: {
2229 doDiscardWithoutConfirmation(true /* show toast */ );
2230 break;
2231 }
2232 case DialogInterface.BUTTON_NEGATIVE: {
2233 // If the user cancels the send, re-enable the send button.
2234 enableSend(true);
2235 break;
2236 }
2237 }
2238
2239 }
2240
Mindy Pereiraefe3d252012-03-01 14:20:44 -08002241 private void doDiscard() {
2242 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2243 .setPositiveButton(R.string.ok, this)
2244 .setNegativeButton(R.string.cancel, null)
2245 .create().show();
2246 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002247 /**
2248 * Effectively discard the current message.
2249 *
2250 * This method is either invoked from the menu or from the dialog
2251 * once the user has confirmed that they want to discard the message.
2252 * @param showToast show "Message discarded" toast if true
2253 */
2254 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002255 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002256 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2257 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07002258 values.put(BaseColumns._ID, mDraftId);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002259 if (mAccount.expungeMessageUri != null) {
2260 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2261 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07002262 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002263 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002264 // This is not strictly necessary (since we should not try to
2265 // save the draft after calling this) but it ensures that if we
2266 // do save again for some reason we make a new draft rather than
2267 // trying to resave an expunged draft.
2268 mDraftId = UIProvider.INVALID_MESSAGE_ID;
2269 }
2270 }
2271
2272 if (showToast) {
2273 // Display a toast to let the user know
2274 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2275 }
2276
2277 // This prevents the draft from being saved in onPause().
2278 discardChanges();
2279 finish();
2280 }
2281
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002282 private void saveIfNeeded() {
2283 if (mAccount == null) {
2284 // We have not chosen an account yet so there's no way that we can save. This is ok,
2285 // though, since we are saving our state before AccountsActivity is activated. Thus, the
2286 // user has not interacted with us yet and there is no real state to save.
2287 return;
2288 }
2289
2290 if (shouldSave()) {
2291 doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
2292 }
2293 }
2294
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002295 @Override
2296 public void onAttachmentDeleted() {
2297 mAttachmentsChanged = true;
2298 updateSaveUi();
2299 }
Mindy Pereira75f66632012-01-11 11:42:02 -08002300
2301
2302 /**
2303 * This is called any time one of our text fields changes.
2304 */
Marc Blank0bbc8582012-04-23 15:07:57 -07002305 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002306 public void afterTextChanged(Editable s) {
2307 mTextChanged = true;
2308 updateSaveUi();
2309 }
2310
2311 @Override
2312 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2313 // Do nothing.
2314 }
2315
Marc Blank0bbc8582012-04-23 15:07:57 -07002316 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08002317 public void onTextChanged(CharSequence s, int start, int before, int count) {
2318 // Do nothing.
2319 }
2320
2321
2322 // There is a big difference between the text associated with an address changing
2323 // to add the display name or to format properly and a recipient being added or deleted.
2324 // Make sure we only notify of changes when a recipient has been added or deleted.
2325 private class RecipientTextWatcher implements TextWatcher {
2326 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2327
2328 private RecipientEditTextView mView;
2329
2330 private TextWatcher mListener;
2331
2332 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2333 mView = view;
2334 mListener = listener;
2335 }
2336
2337 @Override
2338 public void afterTextChanged(Editable s) {
2339 if (hasChanged()) {
2340 mListener.afterTextChanged(s);
2341 }
2342 }
2343
2344 private boolean hasChanged() {
2345 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2346 int totalCount = currRecips.length;
2347 int totalPrevCount = 0;
2348 for (Entry<String, Integer> entry : mContent.entrySet()) {
2349 totalPrevCount += entry.getValue();
2350 }
2351 if (totalCount != totalPrevCount) {
2352 return true;
2353 }
2354
2355 for (String recip : currRecips) {
2356 if (!mContent.containsKey(recip)) {
2357 return true;
2358 } else {
2359 int count = mContent.get(recip) - 1;
2360 if (count < 0) {
2361 return true;
2362 } else {
2363 mContent.put(recip, count);
2364 }
2365 }
2366 }
2367 return false;
2368 }
2369
2370 private String[] tokenizeRecips(String[] recips) {
2371 // Tokenize them all and put them in the list.
2372 String[] recipAddresses = new String[recips.length];
2373 for (int i = 0; i < recips.length; i++) {
2374 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2375 }
2376 return recipAddresses;
2377 }
2378
2379 @Override
2380 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2381 String[] recips = tokenizeRecips(getAddressesFromList(mView));
2382 for (String recip : recips) {
2383 if (!mContent.containsKey(recip)) {
2384 mContent.put(recip, 1);
2385 } else {
2386 mContent.put(recip, (mContent.get(recip)) + 1);
2387 }
2388 }
2389 }
2390
2391 @Override
2392 public void onTextChanged(CharSequence s, int start, int before, int count) {
2393 // Do nothing.
2394 }
2395 }
Vikram Aggarwal8183d452012-04-17 09:13:29 -07002396}