blob: 38a449db131fd1d13e8eb40bb28c183656fa8ef2 [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
Minh Pham47d8d442011-12-13 17:07:13 -080017package com.android.email.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Mindy Pereirac17d0732011-12-29 10:46:19 -080019import android.accounts.Account;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080020import android.animation.Animator;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
Mindy Pereira326c6602012-01-04 15:32:42 -080023import android.app.ActionBar;
24import android.app.ActionBar.OnNavigationListener;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080025import android.app.Activity;
Mindy Pereira6349a042012-01-04 11:25:01 -080026import android.content.ContentResolver;
27import android.content.Context;
28import android.content.Intent;
29import android.database.Cursor;
30import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080031import android.os.Bundle;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080032import android.text.Editable;
33import android.text.TextUtils;
34import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080035import android.text.util.Rfc822Tokenizer;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080036import android.util.TimingLogger;
Mindy Pereira326c6602012-01-04 15:32:42 -080037import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080038import android.view.Menu;
39import android.view.MenuInflater;
40import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080041import android.view.View;
Mindy Pereira326c6602012-01-04 15:32:42 -080042import android.view.ViewGroup;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080043import android.view.View.OnClickListener;
Mindy Pereira1a95a572012-01-05 12:21:29 -080044import android.widget.AdapterView;
45import android.widget.AdapterView.OnItemSelectedListener;
Mindy Pereira326c6602012-01-04 15:32:42 -080046import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080047import android.widget.Button;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080048import android.widget.LinearLayout;
Mindy Pereira1a95a572012-01-05 12:21:29 -080049import android.widget.Spinner;
Mindy Pereira6349a042012-01-04 11:25:01 -080050import android.widget.TextView;
Mindy Pereira7b56a612011-12-14 12:32:28 -080051
Mindy Pereirac17d0732011-12-29 10:46:19 -080052import com.android.common.Rfc822Validator;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080053import com.android.email.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080054import com.android.email.providers.Address;
Mindy Pereira6349a042012-01-04 11:25:01 -080055import com.android.email.providers.UIProvider;
Mindy Pereira326c6602012-01-04 15:32:42 -080056import com.android.email.providers.Attachment;
Mindy Pereira7b56a612011-12-14 12:32:28 -080057import com.android.email.providers.protos.mock.MockAttachment;
Minh Pham47d8d442011-12-13 17:07:13 -080058import com.android.email.R;
Mindy Pereira1a95a572012-01-05 12:21:29 -080059import com.android.email.utils.AccountUtils;
Mindy Pereira7b56a612011-12-14 12:32:28 -080060import com.android.email.utils.MimeType;
Mindy Pereira6349a042012-01-04 11:25:01 -080061import com.android.email.utils.Utils;
Mindy Pereirac17d0732011-12-29 10:46:19 -080062import com.android.ex.chips.RecipientEditTextView;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080063import com.google.common.annotations.VisibleForTesting;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080064import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080065
Mindy Pereira46ce0b12012-01-05 10:32:15 -080066import java.text.DateFormat;
67import java.util.ArrayList;
68import java.util.Arrays;
69import java.util.Collection;
Mindy Pereira1a95a572012-01-05 12:21:29 -080070import java.util.Collections;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080071import java.util.Date;
72import java.util.HashSet;
73import java.util.List;
Mindy Pereira4a27ea92012-01-05 15:55:25 -080074import java.util.Set;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080075
76public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira1a95a572012-01-05 12:21:29 -080077 RespondInlineListener, OnItemSelectedListener {
Mindy Pereira6349a042012-01-04 11:25:01 -080078 // Identifiers for which type of composition this is
79 static final int COMPOSE = -1; // also used for editing a draft
80 static final int REPLY = 0;
81 static final int REPLY_ALL = 1;
82 static final int FORWARD = 2;
83
Mindy Pereira46ce0b12012-01-05 10:32:15 -080084 // HTML tags used to quote reply content
85 // The following style must be in-sync with
86 // pinto.app.MessageUtil.QUOTE_STYLE and
87 // java/com/google/caribou/ui/pinto/modules/app/messageutil.js
88 // BEG_QUOTE_BIDI is also available there when we support BIDI
89 private static final String BLOCKQUOTE_BEGIN = "<blockquote class=\"quote\" style=\""
90 + "margin:0 0 0 .8ex;" + "border-left:1px #ccc solid;" + "padding-left:1ex\">";
91 private static final String BLOCKQUOTE_END = "</blockquote>";
92 // HTML tags used to quote replies & forwards
93 /* package for testing */static final String QUOTE_BEGIN = "<div class=\"quote\">";
94 private static final String QUOTE_END = "</div>";
95 // Separates the attribution headers (Subject, To, etc) from the body in
96 // quoted text.
97 /* package for testing */ static final String HEADER_SEPARATOR = "<br type='attribution'>";
98 private static final int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length();
99
Mindy Pereira6349a042012-01-04 11:25:01 -0800100 // Integer extra holding one of the above compose action
101 private static final String EXTRA_ACTION = "action";
102
103 /**
104 * Notifies the {@code Activity} that the caller is an Email
105 * {@code Activity}, so that the back behavior may be modified accordingly.
106 *
107 * @see #onAppUpPressed
108 */
109 private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
110
111 // If this is a reply/forward then this extra will hold the original message uri
112 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-uri";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800113 private static final String END_TOKEN = ", ";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800114
Mindy Pereirac17d0732011-12-29 10:46:19 -0800115 private RecipientEditTextView mTo;
116 private RecipientEditTextView mCc;
117 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800118 private Button mCcBccButton;
119 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800120 private AttachmentsView mAttachmentsView;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800121 private String mAccount;
122 private Rfc822Validator mRecipientValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800123 private Uri mRefMessageUri;
124 private TextView mSubject;
125
Mindy Pereira326c6602012-01-04 15:32:42 -0800126 private ActionBar mActionBar;
127 private ComposeModeAdapter mComposeModeAdapter;
128 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800129 private boolean mForward;
130 private String mRecipient;
131 private boolean mAttachmentsChanged;
132 private QuotedTextView mQuotedTextView;
133 private TextView mBodyText;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800134 private View mFromStatic;
135 private View mFromSpinner;
136 private Spinner mFrom;
137 private List<String[]> mReplyFromAccounts;
138 private boolean mAccountSpinnerReady;
139 private String[] mCurrentReplyFromAccount;
140 private boolean mMessageIsForwardOrReply;
141 private List<String> mAccounts;
Mindy Pereira326c6602012-01-04 15:32:42 -0800142
143 /**
144 * Can be called from a non-UI thread.
145 */
146 public static void editDraft(Context context, String account, long mLocalMessageId) {
147 }
148
Mindy Pereira6349a042012-01-04 11:25:01 -0800149 /**
150 * Can be called from a non-UI thread.
151 */
152 public static void compose(Context launcher, String account) {
153 launch(launcher, account, null, COMPOSE);
154 }
155
156 /**
157 * Can be called from a non-UI thread.
158 */
159 public static void reply(Context launcher, String account, String uri) {
160 launch(launcher, account, uri, REPLY);
161 }
162
163 /**
164 * Can be called from a non-UI thread.
165 */
166 public static void replyAll(Context launcher, String account, String uri) {
167 launch(launcher, account, uri, REPLY_ALL);
168 }
169
170 /**
171 * Can be called from a non-UI thread.
172 */
173 public static void forward(Context launcher, String account, String uri) {
174 launch(launcher, account, uri, FORWARD);
175 }
176
177 private static void launch(Context launcher, String account, String uri, int action) {
178 Intent intent = new Intent(launcher, ComposeActivity.class);
179 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
180 intent.putExtra(EXTRA_ACTION, action);
181 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
182 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, uri);
183 launcher.startActivity(intent);
184 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800185
186 @Override
187 public void onCreate(Bundle savedInstanceState) {
188 super.onCreate(savedInstanceState);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800189 Intent intent = getIntent();
Mindy Pereira3528d362012-01-05 14:39:44 -0800190 mAccount = intent.getStringExtra(Utils.EXTRA_ACCOUNT);
191 setContentView(R.layout.compose);
192 findViews();
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800193 int action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800194 if (action == REPLY || action == REPLY_ALL || action == FORWARD) {
195 mRefMessageUri = Uri.parse(intent.getStringExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI));
196 initFromRefMessage(action, mAccount);
197 } else {
198 setQuotedTextVisibility(false);
199 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800200 initActionBar(action);
201 asyncInitFromSpinner();
202 }
203
204 @Override
205 protected void onResume() {
206 super.onResume();
207 // Update the from spinner as other accounts
208 // may now be available.
209 asyncInitFromSpinner();
210 }
211
212 private void asyncInitFromSpinner() {
213 Account[] result = AccountUtils.getSyncingAccounts(this, null, null, null);
214 mAccounts = AccountUtils
215 .mergeAccountLists(mAccounts, result, true /* prioritizeAccountList */);
216 createReplyFromCache();
217 initFromSpinner();
218 }
219
220 /**
221 * Create a cache of all accounts a user could send mail from
222 */
223 private void createReplyFromCache() {
224 // Check for replyFroms.
225 List<String> accounts = null;
226 mReplyFromAccounts = new ArrayList<String[]>();
227
228 if (mMessageIsForwardOrReply) {
229 accounts = Collections.singletonList(mAccount);
230 } else {
231 accounts = mAccounts;
232 }
233 for (String account : accounts) {
234 // First add the account. First position is account, second
235 // is display of account, 3rd position is the REAL account this
236 // is being sent from / synced to.
237 mReplyFromAccounts.add(new String[] {
238 account, account, account, "false"
239 });
240 }
241 }
242
243 private void initFromSpinner() {
244 // If there are not yet any accounts in the cached synced accounts
245 // because this is the first time Gmail was opened, and it was opened directly
246 // to the compose activity,don't bother populating the reply from spinner yet.
247 if (mReplyFromAccounts == null || mReplyFromAccounts.size() == 0) {
248 mAccountSpinnerReady = false;
249 return;
250 }
251 FromAddressSpinnerAdapter adapter = new FromAddressSpinnerAdapter(this);
252 int currentAccountIndex = 0;
253 String replyFromAccount = mAccount;
254
255 boolean checkRealAccount = mRecipient == null || mAccount.equals(mRecipient);
256
257 currentAccountIndex = addAccountsToAdapter(adapter, checkRealAccount, replyFromAccount);
258
259 mFrom.setAdapter(adapter);
260 mFrom.setSelection(currentAccountIndex, false);
261 mFrom.setOnItemSelectedListener(this);
262 mCurrentReplyFromAccount = mReplyFromAccounts.get(currentAccountIndex);
263
264 hideOrShowFromSpinner();
265 mAccountSpinnerReady = true;
266 adapter.setSpinner(mFrom);
267 }
268
269 private void hideOrShowFromSpinner() {
270 // Determine whether the from account spinner or the static
271 // from text should be show
272 // When the spinner is shown, the static from text
273 // is hidden
274 showFromSpinner(mFrom.getCount() > 1);
275 }
276
277 private int addAccountsToAdapter(FromAddressSpinnerAdapter adapter, boolean checkRealAccount,
278 String replyFromAccount) {
279 int currentIndex = 0;
280 int currentAccountIndex = 0;
281 // Get the position of the current account
282 for (String[] account : mReplyFromAccounts) {
283 // Add the account to the Adapter
284 // The reason that we are not adding the Account array, but adding
285 // the names of each account, is because Account returns a string
286 // that we don't want to display on toString()
287 adapter.add(account);
288 // Compare to the account address, not the real account being
289 // sent from.
290 if (checkRealAccount) {
291 // Need to check the real account and the account address
292 // so that we can send from the correct address on the
293 // correct account when the same address may exist across
294 // multiple accounts.
295 if (account[FromAddressSpinnerAdapter.REAL_ACCOUNT].equals(mAccount)
296 && account[FromAddressSpinnerAdapter.ACCOUNT_ADDRESS]
297 .equals(replyFromAccount)) {
298 currentAccountIndex = currentIndex;
299 }
300 } else {
301 // Just need to check the account address.
302 if (replyFromAccount.equals(
303 account[FromAddressSpinnerAdapter.ACCOUNT_ADDRESS])) {
304 currentAccountIndex = currentIndex;
305 }
306 }
307
308 currentIndex++;
309 }
310 return currentAccountIndex;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800311 }
312
313 private void findViews() {
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800314 mCcBccButton = (Button) findViewById(R.id.add_cc);
315 if (mCcBccButton != null) {
316 mCcBccButton.setOnClickListener(this);
317 }
318 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -0800319 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
Mindy Pereirac17d0732011-12-29 10:46:19 -0800320 mTo = setupRecipients(R.id.to);
321 mCc = setupRecipients(R.id.cc);
322 mBcc = setupRecipients(R.id.bcc);
Mindy Pereira6349a042012-01-04 11:25:01 -0800323 mSubject = (TextView) findViewById(R.id.subject);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800324 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
325 mQuotedTextView.setRespondInlineListener(this);
326 mBodyText = (TextView) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -0800327 mFromStatic = findViewById(R.id.static_from_content);
328 mFromSpinner = findViewById(R.id.spinner_from_content);
329 mFrom = (Spinner) findViewById(R.id.from_picker);
330 }
331
332 /**
333 * Show the static from text view or the spinner
334 * @param showSpinner Whether the spinner should be shown
335 */
336 private void showFromSpinner(boolean showSpinner) {
337 // show/hide the static text
338 mFromStatic.setVisibility(
339 showSpinner ? View.GONE : View.VISIBLE);
340
341 // show/hide the spinner
342 mFromSpinner.setVisibility(
343 showSpinner ? View.VISIBLE : View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800344 }
345
346 private void setQuotedTextVisibility(boolean show) {
347 mQuotedTextView.setVisibility(show ? View.VISIBLE : View.GONE);
Mindy Pereira6349a042012-01-04 11:25:01 -0800348 }
349
Mindy Pereira326c6602012-01-04 15:32:42 -0800350 private void initActionBar(int action) {
351 mComposeMode = action;
352 mActionBar = getActionBar();
353 if (action == ComposeActivity.COMPOSE) {
354 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
355 mActionBar.setTitle(R.string.compose);
356 } else {
357 mActionBar.setTitle(null);
358 if (mComposeModeAdapter == null) {
359 mComposeModeAdapter = new ComposeModeAdapter(this);
360 }
361 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
362 mActionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
363 switch (action) {
364 case ComposeActivity.REPLY:
365 mActionBar.setSelectedNavigationItem(0);
366 break;
367 case ComposeActivity.REPLY_ALL:
368 mActionBar.setSelectedNavigationItem(1);
369 break;
370 case ComposeActivity.FORWARD:
371 mActionBar.setSelectedNavigationItem(2);
372 break;
373 }
374 }
375 }
376
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800377 private void initFromRefMessage(int action, String recipientAddress) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800378 ContentResolver resolver = getContentResolver();
379 Cursor refMessage = resolver.query(mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
380 null, null);
381 if (refMessage != null) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800382 try {
383 refMessage.moveToFirst();
384 setSubject(refMessage, action);
385 // Setup recipients
386 if (action == FORWARD) {
387 mForward = true;
388 }
389 setQuotedTextVisibility(true);
390 initRecipientsFromRefMessageCursor(recipientAddress, refMessage, action);
391 initBodyFromRefMessage(refMessage, action);
392 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
393 updateAttachments(action, refMessage);
394 } else {
395 // Clear the attachments.
396 removeAllAttachments();
397 }
398 updateHideOrShowCcBcc();
399 } finally {
400 refMessage.close();
401 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800402 }
Mindy Pereirac17d0732011-12-29 10:46:19 -0800403 }
404
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800405 private void initBodyFromRefMessage(Cursor refMessage, int action) {
406 boolean forward = action == FORWARD;
407 DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT);
408 Date date = new Date(refMessage.getLong(UIProvider.MESSAGE_DATE_RECEIVED_MS_COLUMN));
409 StringBuffer quotedText = new StringBuffer();
410
411 if (action == ComposeActivity.REPLY || action == ComposeActivity.REPLY_ALL) {
412 quotedText.append(QUOTE_BEGIN);
413 quotedText
414 .append(String.format(
415 getString(R.string.reply_attribution),
416 dateFormat.format(date),
417 Utils.cleanUpString(
418 refMessage.getString(UIProvider.MESSAGE_FROM_COLUMN), true)));
419 quotedText.append(HEADER_SEPARATOR);
420 quotedText.append(BLOCKQUOTE_BEGIN);
421 quotedText.append(refMessage.getString(UIProvider.MESSAGE_BODY_HTML));
422 quotedText.append(BLOCKQUOTE_END);
423 quotedText.append(QUOTE_END);
424 } else if (action == ComposeActivity.FORWARD) {
425 quotedText.append(QUOTE_BEGIN);
426 quotedText
427 .append(String.format(getString(R.string.forward_attribution), Utils
428 .cleanUpString(refMessage.getString(UIProvider.MESSAGE_FROM_COLUMN),
429 true /* remove empty quotes */), dateFormat.format(date), Utils
430 .cleanUpString(refMessage.getString(UIProvider.MESSAGE_SUBJECT_COLUMN),
431 false /* don't remove empty quotes */), Utils.cleanUpString(
432 refMessage.getString(UIProvider.MESSAGE_TO_COLUMN), true)));
433 String ccAddresses = refMessage.getString(UIProvider.MESSAGE_CC_COLUMN);
434 quotedText.append(String.format(getString(R.string.cc_attribution),
435 Utils.cleanUpString(ccAddresses, true /* remove empty quotes */)));
436 }
437 quotedText.append(HEADER_SEPARATOR);
438 quotedText.append(refMessage.getString(UIProvider.MESSAGE_BODY_HTML));
439 quotedText.append(QUOTE_END);
440 setQuotedText(quotedText.toString(), !forward);
441 }
442
443 /**
444 * Fill the quoted text WebView. There is no point in having a "Show quoted
445 * text" checkbox in a forwarded message so make sure mForward is
446 * initialized properly before calling this method so we can hide it.
447 */
448 public void setQuotedText(CharSequence text, boolean allow) {
449 // There is no way to retrieve this string from the WebView once it's
450 // been loaded, so we need to store it here.
451 mQuotedTextView.setQuotedText(text);
452 mQuotedTextView.allowQuotedText(allow);
453 // If there is quoted text, we always allow respond inline, since this
454 // may be a forward.
455 mQuotedTextView.allowRespondInline(true);
456 }
457
458 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800459 // Its possible there is a menu item OR a button.
460 mCc.setVisibility(TextUtils.isEmpty(mCc.getText()) ? View.GONE : View.VISIBLE);
461 mBcc.setVisibility(TextUtils.isEmpty(mCc.getText()) ? View.GONE : View.VISIBLE);
462 if (mCcBccButton != null) {
463 if (!mCc.isShown() || !mBcc.isShown()) {
464 mCcBccButton.setVisibility(View.VISIBLE);
465 mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
466 : R.string.add_bcc_label));
467 } else {
468 mCcBccButton.setVisibility(View.GONE);
469 }
470 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800471 }
472
473 public void removeAllAttachments() {
474 mAttachmentsView.removeAllViews();
475 }
476
477 private void updateAttachments(int action, Cursor refMessage) {
478 // TODO: when we hook up attachments, make this work properly.
479 }
480
481 private void initRecipientsFromRefMessageCursor(String recipientAddress, Cursor refMessage,
482 int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800483 // Don't populate the address if this is a forward.
484 if (action == ComposeActivity.FORWARD) {
485 return;
486 }
487 initReplyRecipients(mAccount, refMessage, action);
488 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800489
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800490 private void initReplyRecipients(String account, Cursor refMessage, int action) {
491 // This is the email address of the current user, i.e. the one composing
492 // the reply.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800493 final String accountEmail = Address.getEmailAddress(account).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800494 String fromAddress = refMessage.getString(UIProvider.MESSAGE_FROM_COLUMN);
495 String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage
496 .getString(UIProvider.MESSAGE_TO_COLUMN));
497 String[] replytoAddresses = Utils.splitCommaSeparatedString(refMessage
498 .getString(UIProvider.MESSAGE_REPLY_TO_COLUMN));
499 final Collection<String> toAddresses = initToRecipients(account, accountEmail, fromAddress,
500 replytoAddresses, sentToAddresses);
501 addToAddresses(toAddresses);
502
503 // If this is a reply, the Cc list is empty. If this is a reply-all, the
504 // Cc list is the union of the To and Cc recipients of the original
505 // message, excluding the current user's email address and any addresses
506 // already
507 // on the To list.
508 if (action == ComposeActivity.REPLY_ALL) {
509 final Set<String> ccAddresses = Sets.newHashSet();
510 addRecipients(accountEmail, ccAddresses, sentToAddresses);
511 addRecipients(accountEmail, ccAddresses, Utils.splitCommaSeparatedString(refMessage
512 .getString(UIProvider.MESSAGE_CC_COLUMN)));
513 addCcAddresses(ccAddresses, toAddresses);
514 }
515 }
516
517 private void addToAddresses(Collection<String> addresses) {
518 addAddressesToList(addresses, mTo);
519 }
520
521 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
522 addCcAddressesToList(tokenizeAddressList(addresses), tokenizeAddressList(toAddresses),
523 mCc);
524 }
525
526 private void addCcAddresses(Collection<String> addresses) {
527 addAddressesToList(tokenizeAddressList(addresses), mCc);
528 }
529
530 @VisibleForTesting
531 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
532 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
533 String address;
534
535 HashSet<String> compareTo = convertToHashSet(compareToList);
536 for (Rfc822Token[] tokens : addresses) {
537 for (int i = 0; i < tokens.length; i++) {
538 address = tokens[i].toString();
539 // Check if this is a duplicate:
540 if (!compareTo.contains(tokens[i].getAddress())) {
541 // Get the address here
542 list.append(address + END_TOKEN);
543 }
544 }
545 }
546 }
547
548 private void addAddressesToList(List<Rfc822Token[]> addresses, RecipientEditTextView list) {
549 String address;
550 for (Rfc822Token[] tokens : addresses) {
551 for (int i = 0; i < tokens.length; i++) {
552 address = tokens[i].toString();
553 list.append(address + END_TOKEN);
554 }
555 }
556 }
557
558 private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
559 HashSet<String> hash = new HashSet<String>();
560 for (Rfc822Token[] tokens : list) {
561 for (int i = 0; i < tokens.length; i++) {
562 hash.add(tokens[i].getAddress());
563 }
564 }
565 return hash;
566 }
567
568 private void addBccAddresses(Collection<String> addresses) {
569 addAddressesToList(addresses, mBcc);
570 }
571
572 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
573 @VisibleForTesting
574 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
575
576 for (String address: addresses) {
577 tokenized.add(Rfc822Tokenizer.tokenize(address));
578 }
579 return tokenized;
580 }
581
582 @VisibleForTesting
583 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
584 for (String address : addresses) {
585 addAddressToList(address, list);
586 }
587 }
588
589 private void addAddressToList(String address, RecipientEditTextView list) {
590 if (address == null || list == null)
591 return;
592
593 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
594
595 for (int i = 0; i < tokens.length; i++) {
596 list.append(tokens[i] + END_TOKEN);
597 }
598 }
599
600 @VisibleForTesting
601 protected Collection<String> initToRecipients(String account, String accountEmail,
602 String senderAddress, String[] replyToAddresses, String[] inToAddresses) {
603 // The To recipient is the reply-to address specified in the original
604 // message, unless it is:
605 // the current user OR a custom from of the current user, in which case
606 // it's the To recipient list of the original message.
607 // OR missing, in which case use the sender of the original message
608 Set<String> toAddresses = Sets.newHashSet();
Mindy Pereira4a20b702012-01-05 16:24:24 -0800609 Address sender = Address.getEmailAddress(senderAddress);
610 if (sender != null && sender.getAddress().equalsIgnoreCase(account)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800611 // The sender address is this account, so reply acts like reply all.
612 toAddresses.addAll(Arrays.asList(inToAddresses));
613 } else if (replyToAddresses != null && replyToAddresses.length != 0) {
614 toAddresses.addAll(Arrays.asList(replyToAddresses));
615 } else {
616 // Check to see if the sender address is one of the user's custom
617 // from addresses.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800618 if (senderAddress != null && sender != null
619 && !accountEmail.equalsIgnoreCase(sender.getAddress())) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800620 // Replying to the sender of the original message is the most
621 // common case.
622 toAddresses.add(senderAddress);
623 } else {
624 // This happens if the user replies to a message they originally
625 // wrote. In this case, "reply" really means "re-send," so we
626 // target the original recipients. This works as expected even
627 // if the user sent the original message to themselves.
628 toAddresses.addAll(Arrays.asList(inToAddresses));
629 }
630 }
631 return toAddresses;
632 }
633
634 private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
635 for (String email : addresses) {
636 // Do not add this account, or any of the custom froms, to the list
637 // of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -0800638 final String recipientAddress = Address.getEmailAddress(email).getAddress();
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800639 if (!account.equalsIgnoreCase(recipientAddress)) {
640 recipients.add(email.replace("\"\"", ""));
641 }
642 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800643 }
644
645 private void setSubject(Cursor refMessage, int action) {
646 String subject = refMessage.getString(UIProvider.MESSAGE_SUBJECT_COLUMN);
647 String prefix;
648 String correctedSubject = null;
649 if (action == ComposeActivity.COMPOSE) {
650 prefix = "";
651 } else if (action == ComposeActivity.FORWARD) {
652 prefix = getString(R.string.forward_subject_label);
653 } else {
654 prefix = getString(R.string.reply_subject_label);
655 }
656
657 // Don't duplicate the prefix
658 if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
659 correctedSubject = subject;
660 } else {
661 correctedSubject = String
662 .format(getString(R.string.formatted_subject), prefix, subject);
663 }
664 mSubject.setText(correctedSubject);
665 }
666
Mindy Pereirac17d0732011-12-29 10:46:19 -0800667 private RecipientEditTextView setupRecipients(int id) {
668 RecipientEditTextView view = (RecipientEditTextView) findViewById(id);
669 view.setAdapter(new RecipientAdapter(this, mAccount));
670 view.setTokenizer(new Rfc822Tokenizer());
671 if (mRecipientValidator == null) {
672 int offset = mAccount.indexOf("@") + 1;
673 String account = mAccount;
674 if (offset > -1) {
675 account = account.substring(mAccount.indexOf("@") + 1);
676 }
677 mRecipientValidator = new Rfc822Validator(account);
678 }
679 view.setValidator(mRecipientValidator);
680 return view;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800681 }
682
683 @Override
684 public void onClick(View v) {
685 int id = v.getId();
686 switch (id) {
687 case R.id.add_cc:
688 case R.id.add_bcc:
689 // Verify that cc/ bcc aren't showing.
690 // Animate in cc/bcc.
691 mCcBccView.show();
692 break;
693 }
694 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800695
696 @Override
697 public boolean onCreateOptionsMenu(Menu menu) {
698 super.onCreateOptionsMenu(menu);
699 MenuInflater inflater = getMenuInflater();
700 inflater.inflate(R.menu.compose_menu, menu);
701 return true;
702 }
703
704 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800705 public boolean onPrepareOptionsMenu(Menu menu) {
706 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
707 if (ccBcc != null) {
708 // Its possible there is a menu item OR a button.
709 boolean ccFieldVisible = mCc.isShown();
710 boolean bccFieldVisible = mBcc.isShown();
711 if (!ccFieldVisible || !bccFieldVisible) {
712 ccBcc.setVisible(true);
713 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
714 : R.string.add_bcc_label));
715 } else {
716 ccBcc.setVisible(false);
717 }
718 }
719 return true;
720 }
721
722 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800723 public boolean onOptionsItemSelected(MenuItem item) {
724 int id = item.getItemId();
725 boolean handled = false;
726 switch (id) {
Mindy Pereira7b56a612011-12-14 12:32:28 -0800727 case R.id.add_attachment:
728 MockAttachment attachment = new MockAttachment();
729 attachment.partId = "0";
730 attachment.name = "testattachment.png";
731 attachment.contentType = MimeType.inferMimeType(attachment.name, null);
732 attachment.originExtras = "";
733 mAttachmentsView.addAttachment(attachment);
734 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800735 case R.id.add_cc_bcc:
736 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -0800737 handled = true;
738 break;
739 }
740 return !handled ? super.onOptionsItemSelected(item) : handled;
741 }
Mindy Pereira326c6602012-01-04 15:32:42 -0800742
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -0800743 private void showCcBccViews() {
744 mCcBccView.show();
745 if (mCcBccButton != null) {
746 mCcBccButton.setVisibility(View.GONE);
747 }
748 }
749
Mindy Pereira326c6602012-01-04 15:32:42 -0800750 @Override
751 public boolean onNavigationItemSelected(int position, long itemId) {
752 if (position == ComposeActivity.REPLY) {
753 mComposeMode = ComposeActivity.REPLY;
754 } else if (position == ComposeActivity.REPLY_ALL) {
755 mComposeMode = ComposeActivity.REPLY_ALL;
756 } else if (position == ComposeActivity.FORWARD) {
757 mComposeMode = ComposeActivity.FORWARD;
758 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800759 initFromRefMessage(mComposeMode, mAccount);
Mindy Pereira326c6602012-01-04 15:32:42 -0800760 return true;
761 }
762
763 private class ComposeModeAdapter extends ArrayAdapter<String> {
764
765 private LayoutInflater mInflater;
766
767 public ComposeModeAdapter(Context context) {
768 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
769 .getStringArray(R.array.compose_modes));
770 }
771
772 private LayoutInflater getInflater() {
773 if (mInflater == null) {
774 mInflater = LayoutInflater.from(getContext());
775 }
776 return mInflater;
777 }
778
779 @Override
780 public View getView(int position, View convertView, ViewGroup parent) {
781 if (convertView == null) {
782 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
783 }
784 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
785 return super.getView(position, convertView, parent);
786 }
787 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800788
789 @Override
790 public void onRespondInline(String text) {
791 appendToBody(text, false);
792 }
793
794 /**
795 * Append text to the body of the message. If there is no existing body
796 * text, just sets the body to text.
797 *
798 * @param text
799 * @param withSignature True to append a signature.
800 */
801 public void appendToBody(CharSequence text, boolean withSignature) {
802 Editable bodyText = mBodyText.getEditableText();
803 if (bodyText != null && bodyText.length() > 0) {
804 bodyText.append(text);
805 } else {
806 setBody(text, withSignature);
807 }
808 }
809
810 /**
811 * Set the body of the message.
812 * @param text
813 * @param withSignature True to append a signature.
814 */
815 public void setBody(CharSequence text, boolean withSignature) {
816 mBodyText.setText(text);
817 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800818
819 @Override
820 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
821 // TODO
822 }
823
824 @Override
825 public void onNothingSelected(AdapterView<?> parent) {
826 // Do nothing.
827 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800828}