1/*
2 * Copyright (C) 2008 The Android Open Source Project
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
17package com.android.email.activity;
18
19import android.content.ContentUris;
20import android.content.Context;
21import android.content.Intent;
22import android.net.Uri;
23import android.test.ActivityInstrumentationTestCase2;
24import android.test.UiThreadTest;
25import android.test.suitebuilder.annotation.LargeTest;
26import android.test.suitebuilder.annotation.SmallTest;
27import android.util.Log;
28import android.view.View;
29import android.widget.EditText;
30import android.widget.MultiAutoCompleteTextView;
31
32import com.android.email.Email;
33import com.android.email.EmailAddressValidator;
34import com.android.email.R;
35import com.android.email.TestUtils;
36import com.android.emailcommon.Logging;
37import com.android.emailcommon.mail.Address;
38import com.android.emailcommon.mail.MessagingException;
39import com.android.emailcommon.provider.Account;
40import com.android.emailcommon.provider.EmailContent.Attachment;
41import com.android.emailcommon.provider.EmailContent.Message;
42import com.google.android.collect.Lists;
43
44import java.util.ArrayList;
45
46
47/**
48 * Various instrumentation tests for MessageCompose.
49 *
50 * It might be possible to convert these to ActivityUnitTest, which would be faster.
51 *
52 * You can run this entire test case with:
53 *   runtest -c com.android.email.activity.MessageComposeTests email
54 */
55@LargeTest
56public class MessageComposeTests
57        extends ActivityInstrumentationTestCase2<MessageCompose> {
58
59    private Context mContext;
60
61    private MultiAutoCompleteTextView mToView;
62    private MultiAutoCompleteTextView mCcView;
63    private MultiAutoCompleteTextView mBccView;
64    private EditText mSubjectView;
65    private EditText mMessageView;
66    private long mCreatedAccountId = -1;
67    private String mSignature;
68
69    private static final String ACCOUNT = "account@android.com";
70    private static final String SENDER = "sender@android.com";
71    private static final String REPLYTO = "replyto@android.com";
72    private static final String RECIPIENT_TO = "recipient-to@android.com";
73    private static final String RECIPIENT_CC = "recipient-cc@android.com";
74    private static final String RECIPIENT_BCC = "recipient-bcc@android.com";
75    private static final String SUBJECT = "This is the subject";
76    private static final String BODY = "This is the body.  This is also the body.";
77    private static final String SIGNATURE = "signature";
78
79    private static final String FROM = "Fred From <from@google.com>";
80    private static final String TO1 = "First To <first.to@google.com>";
81    private static final String TO2 = "Second To <second.to@google.com>";
82    private static final String TO3 = "CopyFirst Cc <first.cc@google.com>";
83    private static final String CC1 = "First Cc <first.cc@google.com>";
84    private static final String CC2 = "Second Cc <second.cc@google.com>";
85    private static final String CC3 = "Third Cc <third.cc@google.com>";
86    private static final String CC4 = "CopySecond To <second.to@google.com>";
87
88    private static final String UTF16_SENDER =
89            "\u3042\u3044\u3046 \u3048\u304A <sender@android.com>";
90    private static final String UTF16_REPLYTO =
91            "\u3042\u3044\u3046\u3048\u304A <replyto@android.com>";
92    private static final String UTF16_RECIPIENT_TO =
93            "\"\u3042\u3044\u3046,\u3048\u304A\" <recipient-to@android.com>";
94    private static final String UTF16_RECIPIENT_CC =
95            "\u30A2\u30AB \u30B5\u30BF\u30CA <recipient-cc@android.com>";
96    private static final String UTF16_RECIPIENT_BCC =
97            "\"\u30A2\u30AB,\u30B5\u30BF\u30CA\" <recipient-bcc@android.com>";
98    private static final String UTF16_SUBJECT = "\u304A\u5BFF\u53F8\u306B\u3059\u308B\uFF1F";
99    private static final String UTF16_BODY = "\u65E5\u672C\u8A9E\u306E\u6587\u7AE0";
100
101    private static final String UTF32_SENDER =
102            "\uD834\uDF01\uD834\uDF46 \uD834\uDF22 <sender@android.com>";
103    private static final String UTF32_REPLYTO =
104            "\uD834\uDF01\uD834\uDF46\uD834\uDF22 <replyto@android.com>";
105    private static final String UTF32_RECIPIENT_TO =
106            "\"\uD834\uDF01\uD834\uDF46,\uD834\uDF22\" <recipient-to@android.com>";
107    private static final String UTF32_RECIPIENT_CC =
108            "\uD834\uDF22 \uD834\uDF01\uD834\uDF46 <recipient-cc@android.com>";
109    private static final String UTF32_RECIPIENT_BCC =
110            "\"\uD834\uDF22,\uD834\uDF01\uD834\uDF46\" <recipient-bcc@android.com>";
111    private static final String UTF32_SUBJECT = "\uD834\uDF01\uD834\uDF46";
112    private static final String UTF32_BODY = "\uD834\uDF01\uD834\uDF46";
113
114    /*
115     * The following action definitions are purposefully copied from MessageCompose, so that
116     * any changes to the action strings will break these tests. Changes to the actions should
117     * be done consciously to think about existing shortcuts and clients.
118     */
119
120    private static final String ACTION_REPLY = "com.android.email.intent.action.REPLY";
121    private static final String ACTION_REPLY_ALL = "com.android.email.intent.action.REPLY_ALL";
122    private static final String ACTION_FORWARD = "com.android.email.intent.action.FORWARD";
123    private static final String ACTION_EDIT_DRAFT = "com.android.email.intent.action.EDIT_DRAFT";
124
125    public MessageComposeTests() {
126        super(MessageCompose.class);
127    }
128
129    /*
130     * The Message Composer activity is only enabled if one or more accounts
131     * are configured on the device and a default account has been specified,
132     * so we do that here before every test.
133     */
134    @Override
135    protected void setUp() throws Exception {
136        super.setUp();
137        mContext = getInstrumentation().getTargetContext();
138
139        // Force assignment of a default account
140        long accountId = Account.getDefaultAccountId(mContext);
141        if (accountId == -1) {
142            Account account = new Account();
143            account.mSenderName = "Bob Sender";
144            account.mEmailAddress = "bob@sender.com";
145            account.mSignature = SIGNATURE;
146            account.save(mContext);
147            accountId = account.mId;
148            mCreatedAccountId = accountId;
149        }
150        Account account = Account.restoreAccountWithId(mContext, accountId);
151        mSignature = account.getSignature();
152        Email.setServicesEnabledSync(mContext);
153
154        Intent intent = new Intent(Intent.ACTION_VIEW);
155        setActivityIntent(intent);
156        final MessageCompose a = getActivity();
157        mToView = (MultiAutoCompleteTextView) a.findViewById(R.id.to);
158        mCcView = (MultiAutoCompleteTextView) a.findViewById(R.id.cc);
159        mBccView = (MultiAutoCompleteTextView) a.findViewById(R.id.bcc);
160        mSubjectView = (EditText) a.findViewById(R.id.subject);
161        mMessageView = (EditText) a.findViewById(R.id.message_content);
162    }
163
164    @Override
165    protected void tearDown() throws Exception {
166        super.tearDown();
167        Context context = getInstrumentation().getTargetContext();
168        // If we created an account, delete it here
169        if (mCreatedAccountId > -1) {
170            context.getContentResolver().delete(
171                    ContentUris.withAppendedId(Account.CONTENT_URI, mCreatedAccountId), null, null);
172        }
173    }
174
175    /**
176     * The name 'test preconditions' is a convention to signal that if this
177     * test doesn't pass, the test case was not set up properly and it might
178     * explain any and all failures in other tests.  This is not guaranteed
179     * to run before other tests, as junit uses reflection to find the tests.
180     */
181    public void testPreconditions() {
182        assertNotNull(mToView);
183        assertEquals(0, mToView.length());
184        assertNotNull(mSubjectView);
185        assertEquals(0, mSubjectView.length());
186        assertNotNull(mMessageView);
187
188        // Note that the signature is always preceeded with a newline.
189        int sigLength = (mSignature == null) ? 0 : (1 + mSignature.length());
190        assertEquals(sigLength, mMessageView.length());
191    }
192
193     /**
194     * Test a couple of variations of processSourceMessage() for REPLY.
195     *   To = Reply-To or From:  (if REPLY)
196     *   To = (Reply-To or From:), Cc = + To: + Cc:   (if REPLY_ALL)
197     *   Subject = Re: Subject
198     *   Body = empty  (and has cursor)
199     *
200     *   TODO test REPLY_ALL
201     */
202    public void testProcessSourceMessageReply() throws MessagingException, Throwable {
203        final Message message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
204        Intent intent = new Intent(ACTION_REPLY);
205        final MessageCompose a = getActivity();
206        a.setIntent(intent);
207        final Account account = new Account();
208        account.mEmailAddress = ACCOUNT;
209
210        runTestOnUiThread(new Runnable() {
211            public void run() {
212                a.processSourceMessage(message, account);
213                a.setInitialComposeText(null, null);
214                checkFields(SENDER + ", ", null, null, "Re: " + SUBJECT, null, null);
215                checkFocused(mMessageView);
216            }
217        });
218
219        message.mFrom = null;
220        message.mReplyTo = Address.parseAndPack(REPLYTO);
221
222        runTestOnUiThread(new Runnable() {
223            public void run() {
224                resetViews();
225                a.processSourceMessage(message, account);
226                a.setInitialComposeText(null, null);
227                checkFields(REPLYTO + ", ", null, null, "Re: " + SUBJECT, null, null);
228                checkFocused(mMessageView);
229            }
230        });
231    }
232
233    /**
234     * Tests similar cases as testProcessSourceMessageReply, but when sender is in From: (or
235     * Reply-to: if applicable) field.
236     *
237     * To = To (if REPLY)
238     * To = To, Cc = Cc (if REPLY_ALL)
239     * Subject = Re: Subject
240     * Body = empty  (and has cursor)
241     *
242     * @throws MessagingException
243     * @throws Throwable
244     */
245    public void testRepliesWithREplyToFields() throws MessagingException, Throwable {
246        final Message message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
247        message.mCc = RECIPIENT_CC;
248        Intent intent = new Intent(ACTION_REPLY);
249        final MessageCompose a = getActivity();
250        a.setIntent(intent);
251        final Account account = new Account();
252        account.mEmailAddress = SENDER;
253
254        runTestOnUiThread(new Runnable() {
255            public void run() {
256                a.processSourceMessage(message, account);
257                a.setInitialComposeText(null, null);
258                checkFields(RECIPIENT_TO + ", ", null, null, "Re: " + SUBJECT, null, null);
259                checkFocused(mMessageView);
260            }
261        });
262
263        message.mFrom = null;
264        message.mReplyTo = Address.parseAndPack(SENDER);
265
266        runTestOnUiThread(new Runnable() {
267            public void run() {
268                resetViews();
269                a.processSourceMessage(message, account);
270                a.setInitialComposeText(null, null);
271                checkFields(RECIPIENT_TO + ", ", null, null, "Re: " + SUBJECT, null, null);
272                checkFocused(mMessageView);
273            }
274        });
275
276        a.setIntent(new Intent(ACTION_REPLY_ALL));
277        runTestOnUiThread(new Runnable() {
278            public void run() {
279                resetViews();
280                a.processSourceMessage(message, account);
281                a.setInitialComposeText(null, null);
282                checkFields(RECIPIENT_TO + ", ", RECIPIENT_CC + ", ", null,
283                        "Re: " + SUBJECT, null, null);
284                checkFocused(mMessageView);
285            }
286        });
287    }
288
289    public void testProcessSourceMessageReplyWithSignature() throws MessagingException, Throwable {
290        final Message message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
291        Intent intent = new Intent(ACTION_REPLY);
292        final MessageCompose a = getActivity();
293        a.setIntent(intent);
294        final Account account = new Account();
295        account.mEmailAddress = ACCOUNT;
296        account.mSignature = SIGNATURE;
297        runTestOnUiThread(new Runnable() {
298            public void run() {
299                a.processSourceMessage(message, account);
300                a.setInitialComposeText(null, SIGNATURE);
301                checkFields(SENDER + ", ", null, null, "Re: " + SUBJECT, null, SIGNATURE);
302                checkFocused(mMessageView);
303            }
304        });
305
306        message.mFrom = null;
307        message.mReplyTo = Address.parseAndPack(REPLYTO);
308
309        runTestOnUiThread(new Runnable() {
310            public void run() {
311                resetViews();
312                a.processSourceMessage(message, account);
313                a.setInitialComposeText(null, SIGNATURE);
314                checkFields(REPLYTO + ", ", null, null, "Re: " + SUBJECT, null, SIGNATURE);
315                checkFocused(mMessageView);
316            }
317        });
318    }
319
320    public void testProcessSourceMessageForwardWithSignature()
321            throws MessagingException, Throwable {
322        final Message message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
323        Intent intent = new Intent(ACTION_FORWARD);
324        final MessageCompose a = getActivity();
325        a.setIntent(intent);
326        final Account account = new Account();
327        account.mSignature = SIGNATURE;
328        runTestOnUiThread(new Runnable() {
329            public void run() {
330                a.processSourceMessage(message, account);
331                a.setInitialComposeText(null, SIGNATURE);
332                checkFields(null, null, null, "Fwd: " + SUBJECT, null, SIGNATURE);
333                checkFocused(mToView);
334           }
335        });
336    }
337
338    /**
339     * Test reply to utf-16 name and address
340     */
341    public void testProcessSourceMessageReplyUtf16() throws MessagingException, Throwable {
342        final Message message = buildTestMessage(UTF16_RECIPIENT_TO, UTF16_SENDER,
343                UTF16_SUBJECT, UTF16_BODY);
344        Intent intent = new Intent(ACTION_REPLY);
345        final MessageCompose a = getActivity();
346        a.setIntent(intent);
347        final Account account = new Account();
348        account.mEmailAddress = ACCOUNT;
349
350        runTestOnUiThread(new Runnable() {
351            public void run() {
352                a.processSourceMessage(message, account);
353                a.setInitialComposeText(null, null);
354                checkFields(UTF16_SENDER + ", ", null, null, "Re: " + UTF16_SUBJECT, null, null);
355                checkFocused(mMessageView);
356            }
357        });
358
359        message.mFrom = null;
360        message.mReplyTo = Address.parseAndPack(UTF16_REPLYTO);
361
362        runTestOnUiThread(new Runnable() {
363            public void run() {
364                resetViews();
365                a.processSourceMessage(message, account);
366                a.setInitialComposeText(null, null);
367                checkFields(UTF16_REPLYTO + ", ", null, null, "Re: " + UTF16_SUBJECT, null, null);
368                checkFocused(mMessageView);
369            }
370        });
371    }
372
373    /**
374     * Test reply to utf-32 name and address
375     */
376    public void testProcessSourceMessageReplyUtf32() throws MessagingException, Throwable {
377        final Message message = buildTestMessage(UTF32_RECIPIENT_TO, UTF32_SENDER,
378                UTF32_SUBJECT, UTF32_BODY);
379        Intent intent = new Intent(ACTION_REPLY);
380        final MessageCompose a = getActivity();
381        a.setIntent(intent);
382        final Account account = new Account();
383        account.mEmailAddress = ACCOUNT;
384
385        runTestOnUiThread(new Runnable() {
386            public void run() {
387                a.processSourceMessage(message, account);
388                a.setInitialComposeText(null, null);
389                checkFields(UTF32_SENDER + ", ", null, null, "Re: " + UTF32_SUBJECT, null, null);
390                checkFocused(mMessageView);
391            }
392        });
393
394        message.mFrom = null;
395        message.mReplyTo = Address.parseAndPack(UTF32_REPLYTO);
396
397        runTestOnUiThread(new Runnable() {
398            public void run() {
399                resetViews();
400                a.processSourceMessage(message, account);
401                a.setInitialComposeText(null, null);
402                checkFields(UTF32_REPLYTO + ", ", null, null, "Re: " + UTF32_SUBJECT, null, null);
403                checkFocused(mMessageView);
404            }
405        });
406    }
407
408    /**
409     * Test processSourceMessage() for FORWARD
410     *   To = empty  (and has cursor)
411     *   Subject = Fwd: Subject
412     *   Body = empty
413     */
414    public void testProcessSourceMessageForward() throws MessagingException, Throwable {
415        final Message message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
416        Intent intent = new Intent(ACTION_FORWARD);
417        final MessageCompose a = getActivity();
418        a.setIntent(intent);
419
420        runTestOnUiThread(new Runnable() {
421            public void run() {
422                a.processSourceMessage(message, null);
423                a.setInitialComposeText(null, null);
424                checkFields(null, null, null, "Fwd: " + SUBJECT, null, null);
425                checkFocused(mToView);
426            }
427        });
428    }
429
430    /**
431     * Test processSourceMessage() for EDIT_DRAFT
432     * Reply and ReplyAll should map:
433     *   To = to
434     *   Subject = Subject
435     *   Body = body (has cursor)
436     *
437     * TODO check CC and BCC handling too
438     */
439    public void testProcessDraftMessage() throws MessagingException, Throwable {
440
441        final Message message = buildTestMessage(RECIPIENT_TO, SENDER, SUBJECT, BODY);
442        Intent intent = new Intent(ACTION_EDIT_DRAFT);
443        final MessageCompose a = getActivity();
444        a.setIntent(intent);
445
446        runTestOnUiThread(new Runnable() {
447            public void run() {
448                a.processDraftMessage(message, true);
449                checkFields(RECIPIENT_TO + ", ", null, null, SUBJECT, BODY, null);
450                checkFocused(mMessageView);
451            }
452        });
453
454        // if subject is null, then cursor should be there instead
455
456        message.mSubject = "";
457
458        runTestOnUiThread(new Runnable() {
459            public void run() {
460                resetViews();
461                a.processDraftMessage(message, true);
462                checkFields(RECIPIENT_TO + ", ", null, null, null, BODY, null);
463                checkFocused(mSubjectView);
464            }
465        });
466
467    }
468
469    /**
470     * Test processDraftMessage() for EDIT_DRAFT with utf-16 name and address
471     * TODO check CC and BCC handling too
472     */
473    public void testProcessDraftMessageWithUtf16() throws MessagingException, Throwable {
474
475        final Message message = buildTestMessage(UTF16_RECIPIENT_TO, UTF16_SENDER,
476                UTF16_SUBJECT, UTF16_BODY);
477        Intent intent = new Intent(ACTION_EDIT_DRAFT);
478        final MessageCompose a = getActivity();
479        a.setIntent(intent);
480
481        runTestOnUiThread(new Runnable() {
482            public void run() {
483                a.processDraftMessage(message, true);
484                checkFields(UTF16_RECIPIENT_TO + ", ",
485                        null, null, UTF16_SUBJECT, UTF16_BODY, null);
486                checkFocused(mMessageView);
487            }
488        });
489
490        // if subject is null, then cursor should be there instead
491
492        message.mSubject = "";
493
494        runTestOnUiThread(new Runnable() {
495            public void run() {
496                resetViews();
497                a.processDraftMessage(message, true);
498                checkFields(UTF16_RECIPIENT_TO + ", ", null, null, null, UTF16_BODY, null);
499                checkFocused(mSubjectView);
500            }
501        });
502
503    }
504
505    /**
506     * Test processDraftMessage() for EDIT_DRAFT with utf-32 name and address
507     * TODO check CC and BCC handling too
508     */
509    public void testProcessDraftMessageWithUtf32() throws MessagingException, Throwable {
510
511        final Message message = buildTestMessage(UTF32_RECIPIENT_TO, UTF32_SENDER,
512                UTF32_SUBJECT, UTF32_BODY);
513        Intent intent = new Intent(ACTION_EDIT_DRAFT);
514        final MessageCompose a = getActivity();
515        a.setIntent(intent);
516
517        runTestOnUiThread(new Runnable() {
518            public void run() {
519                a.processDraftMessage(message, true);
520                checkFields(UTF32_RECIPIENT_TO + ", ",
521                        null, null, UTF32_SUBJECT, UTF32_BODY, null);
522                checkFocused(mMessageView);
523            }
524        });
525
526        // if subject is null, then cursor should be there instead
527
528        message.mSubject = "";
529
530        runTestOnUiThread(new Runnable() {
531            public void run() {
532                resetViews();
533                a.processDraftMessage(message, true);
534                checkFields(UTF32_RECIPIENT_TO + ", ", null, null, null, UTF32_BODY, null);
535                checkFocused(mSubjectView);
536            }
537        });
538
539    }
540
541    /**
542     * Check that we create the proper to and cc addressees in reply and reply-all, making sure
543     * to reject duplicate addressees AND the email address of the sending account
544     *
545     * In this case, we're doing a "reply"
546     * The user is TO1 (a "to" recipient)
547     * The to should be: FROM
548     * The cc should be empty
549     */
550    public void testReplyAddresses() throws Throwable {
551        final MessageCompose a = getActivity();
552        // Doesn't matter what Intent we use here
553        final Intent intent = new Intent(Intent.ACTION_VIEW);
554        Message msg = new Message();
555        final Account account = new Account();
556
557        msg.mFrom = Address.parseAndPack(FROM);
558        msg.mTo = Address.parseAndPack(TO1 + ',' + TO2);
559        msg.mCc = Address.parseAndPack(CC1 + ',' + CC2 + ',' + CC3);
560        final Message message = msg;
561        account.mEmailAddress = "FiRsT.tO@gOoGlE.cOm";
562
563        runTestOnUiThread(new Runnable() {
564            public void run() {
565                a.initFromIntent(intent);
566                a.setupAddressViews(message, account, false);
567                assertEquals("", mCcView.getText().toString());
568                String result = Address.parseAndPack(mToView.getText().toString());
569                String expected = Address.parseAndPack(FROM);
570                assertEquals(expected, result);
571
572                // It doesn't harm even if the CC view is visible.
573            }
574        });
575    }
576
577    /**
578     * Check that we create the proper to and cc addressees in reply and reply-all, making sure
579     * to reject duplicate addressees AND the email address of the sending account
580     *
581     * In this case, we're doing a "reply all"
582     * The user is TO1 (a "to" recipient)
583     * The to should be: FROM
584     * The cc should be: TO2, CC1, CC2, and CC3
585     */
586    public void testReplyAllAddresses1() throws Throwable {
587        final MessageCompose a = getActivity();
588        // Doesn't matter what Intent we use here
589        final Intent intent = new Intent(Intent.ACTION_VIEW);
590        Message msg = new Message();
591        final Account account = new Account();
592
593        msg.mFrom = Address.parseAndPack(FROM);
594        msg.mTo = Address.parseAndPack(TO1 + ',' + TO2);
595        msg.mCc = Address.parseAndPack(CC1 + ',' + CC2 + ',' + CC3);
596        final Message message = msg;
597        account.mEmailAddress = "FiRsT.tO@gOoGlE.cOm";
598
599        runTestOnUiThread(new Runnable() {
600            public void run() {
601                a.initFromIntent(intent);
602                a.setupAddressViews(message, account, true);
603                String result = Address.parseAndPack(mToView.getText().toString());
604                String expected = Address.parseAndPack(FROM);
605                assertEquals(expected, result);
606                result = Address.parseAndPack(mCcView.getText().toString());
607                expected = Address.parseAndPack(TO2 + ',' + CC1 + ',' + CC2 + ',' + CC3);
608                assertEquals(expected, result);
609                TestUtils.assertViewVisible(mCcView);
610            }
611        });
612    }
613
614    /**
615     * Check that we create the proper to and cc addressees in reply and reply-all, making sure
616     * to reject duplicate addressees AND the email address of the sending account
617     *
618     * In this case, we're doing a "reply all"
619     * The user is CC2 (a "cc" recipient)
620     * The to should be: FROM,
621     * The cc should be: TO1, TO2, CC1 and CC3 (CC2 is our account's email address)
622     */
623    public void testReplyAllAddresses2() throws Throwable {
624        final MessageCompose a = getActivity();
625        // Doesn't matter what Intent we use here
626        final Intent intent = new Intent(Intent.ACTION_VIEW);
627        Message msg = new Message();
628        final Account account = new Account();
629
630        msg.mFrom = Address.parseAndPack(FROM);
631        msg.mTo = Address.parseAndPack(TO1 + ',' + TO2);
632        msg.mCc = Address.parseAndPack(CC1 + ',' + CC2 + ',' + CC3);
633        final Message message = msg;
634        account.mEmailAddress = "sEcOnD.cC@gOoGlE.cOm";
635
636        runTestOnUiThread(new Runnable() {
637            public void run() {
638                a.initFromIntent(intent);
639                a.setupAddressViews(message, account, true);
640                String result = Address.parseAndPack(mToView.getText().toString());
641                String expected = Address.parseAndPack(FROM);
642                assertEquals(expected, result);
643                result = Address.parseAndPack(mCcView.getText().toString());
644                expected = Address.parseAndPack(TO1 + ',' + TO2 + ',' + CC1 + ',' + CC3);
645                assertEquals(expected, result);
646                TestUtils.assertViewVisible(mCcView);
647            }
648        });
649    }
650
651    /**
652     * Check that we create the proper to and cc addressees in reply and reply-all, making sure
653     * to reject duplicate addressees AND the email address of the sending account
654     *
655     * In this case, we're doing a "reply all"
656     * The user is CC2 (a "cc" recipient)
657     * The to should be: FROM
658     * The cc should be: TO1, TO2, ,TO3, and CC3 (CC1/CC4 are duplicates; CC2 is the our
659     * account's email address)
660     */
661    public void testReplyAllAddresses3() throws Throwable {
662        final MessageCompose a = getActivity();
663        // Doesn't matter what Intent we use here
664        final Intent intent = new Intent(Intent.ACTION_VIEW);
665        Message msg = new Message();
666        final Account account = new Account();
667
668        msg.mFrom = Address.parseAndPack(FROM);
669        msg.mTo = Address.parseAndPack(TO1 + ',' + TO2 + ',' + TO3);
670        msg.mCc = Address.parseAndPack(CC1 + ',' + CC2 + ',' + CC3 + ',' + CC4);
671        final Message message = msg;
672        account.mEmailAddress = "sEcOnD.cC@gOoGlE.cOm";
673
674        runTestOnUiThread(new Runnable() {
675            public void run() {
676                a.initFromIntent(intent);
677                a.setupAddressViews(message, account, true);
678                String result = Address.parseAndPack(mToView.getText().toString());
679                String expected = Address.parseAndPack(FROM);
680                assertEquals(expected, result);
681                result = Address.parseAndPack(mCcView.getText().toString());
682                expected = Address.parseAndPack(TO1 + ',' + TO2 + ',' + TO3+ ',' + CC3);
683                assertEquals(expected, result);
684                TestUtils.assertViewVisible(mCcView);
685            }
686        });
687    }
688
689    /**
690     * Test for processing of Intent EXTRA_* fields that impact the headers:
691     *   Intent.EXTRA_EMAIL, Intent.EXTRA_CC, Intent.EXTRA_BCC, Intent.EXTRA_SUBJECT
692     */
693    public void testIntentHeaderExtras() throws MessagingException, Throwable {
694
695        Intent intent = new Intent(Intent.ACTION_VIEW);
696        intent.putExtra(Intent.EXTRA_EMAIL, new String[] { RECIPIENT_TO });
697        intent.putExtra(Intent.EXTRA_CC, new String[] { RECIPIENT_CC });
698        intent.putExtra(Intent.EXTRA_BCC, new String[] { RECIPIENT_BCC });
699        intent.putExtra(Intent.EXTRA_SUBJECT, SUBJECT);
700
701        final MessageCompose a = getActivity();
702        final Intent i2 = new Intent(intent);
703
704        runTestOnUiThread(new Runnable() {
705            public void run() {
706                a.initFromIntent(i2);
707                checkFields(RECIPIENT_TO + ", ", RECIPIENT_CC + ", ", RECIPIENT_BCC + ", ", SUBJECT,
708                        null, mSignature);
709                checkFocused(mMessageView);
710            }
711        });
712    }
713
714    /**
715     * Test for processing of Intent EXTRA_* fields that impact the headers with utf-16.
716     */
717    public void testIntentHeaderExtrasWithUtf16() throws MessagingException, Throwable {
718
719        Intent intent = new Intent(Intent.ACTION_VIEW);
720        intent.putExtra(Intent.EXTRA_EMAIL, new String[] { UTF16_RECIPIENT_TO });
721        intent.putExtra(Intent.EXTRA_CC, new String[] { UTF16_RECIPIENT_CC });
722        intent.putExtra(Intent.EXTRA_BCC, new String[] { UTF16_RECIPIENT_BCC });
723        intent.putExtra(Intent.EXTRA_SUBJECT, UTF16_SUBJECT);
724
725        final MessageCompose a = getActivity();
726        final Intent i2 = new Intent(intent);
727
728        runTestOnUiThread(new Runnable() {
729            public void run() {
730                a.initFromIntent(i2);
731                checkFields(UTF16_RECIPIENT_TO + ", ", UTF16_RECIPIENT_CC + ", ",
732                        UTF16_RECIPIENT_BCC + ", ", UTF16_SUBJECT, null, mSignature);
733                checkFocused(mMessageView);
734            }
735        });
736    }
737
738    /**
739     * Test for processing of Intent EXTRA_* fields that impact the headers with utf-32.
740     */
741    public void testIntentHeaderExtrasWithUtf32() throws MessagingException, Throwable {
742
743        Intent intent = new Intent(Intent.ACTION_VIEW);
744        intent.putExtra(Intent.EXTRA_EMAIL, new String[] { UTF32_RECIPIENT_TO });
745        intent.putExtra(Intent.EXTRA_CC, new String[] { UTF32_RECIPIENT_CC });
746        intent.putExtra(Intent.EXTRA_BCC, new String[] { UTF32_RECIPIENT_BCC });
747        intent.putExtra(Intent.EXTRA_SUBJECT, UTF32_SUBJECT);
748
749        final MessageCompose a = getActivity();
750        final Intent i2 = new Intent(intent);
751
752        runTestOnUiThread(new Runnable() {
753            public void run() {
754                a.initFromIntent(i2);
755                checkFields(UTF32_RECIPIENT_TO + ", ", UTF32_RECIPIENT_CC + ", ",
756                        UTF32_RECIPIENT_BCC + ", ", UTF32_SUBJECT, null, mSignature);
757                checkFocused(mMessageView);
758            }
759        });
760    }
761
762    /**
763     * Test for processing of a typical browser "share" intent, e.g.
764     * type="text/plain", EXTRA_TEXT="http:link.server.com"
765     */
766    public void testIntentSendPlainText() throws MessagingException, Throwable {
767
768        Intent intent = new Intent(Intent.ACTION_SEND);
769        intent.setType("text/plain");
770        intent.putExtra(Intent.EXTRA_TEXT, BODY);
771
772        final MessageCompose a = getActivity();
773        final Intent i2 = new Intent(intent);
774
775        runTestOnUiThread(new Runnable() {
776            public void run() {
777                a.initFromIntent(i2);
778                checkFields(null, null, null, null, BODY, mSignature);
779                checkFocused(mToView);
780            }
781        });
782    }
783
784    /**
785     * Test for processing of a typical browser Mailto intent, e.g.
786     * action=android.intent.action.VIEW
787     * categories={android.intent.category.BROWSABLE}
788     * data=mailto:user@domain.com?subject=This%20is%20%the%subject
789     */
790    public void testBrowserMailToIntent() throws MessagingException, Throwable {
791
792        Intent intent = new Intent(Intent.ACTION_VIEW);
793        Uri uri = Uri.parse("mailto:" + RECIPIENT_TO + "?subject=This%20is%20the%20subject");
794        intent.setData(uri);
795
796        final MessageCompose a = getActivity();
797        final Intent i2 = new Intent(intent);
798
799        runTestOnUiThread(new Runnable() {
800            public void run() {
801                a.initFromIntent(i2);
802                checkFields(
803                        RECIPIENT_TO + ", ", null, null, "This is the subject", null, mSignature);
804                checkFocused(mMessageView);
805            }
806        });
807    }
808
809    /**
810     * TODO: test mailto: with simple encoding mode
811     * TODO: test mailto: URI with all optional fields
812     * TODO: come up with a way to add a very small attachment
813     * TODO: confirm the various details between handling of SEND, VIEW, SENDTO
814     */
815
816    /**
817     * Helper method to quickly check (and assert) on the to, subject, and content views.
818     *
819     * @param to expected value (null = it must be empty)
820     * @param cc expected value (null = it must be empty)
821     * @param bcc expected value (null = it must be empty)
822     * @param subject expected value (null = it must be empty)
823     * @param content expected value (null = it must be empty)
824     * @param signature expected value (null = it must be empty)
825     */
826    private void checkFields(String to, String cc, String bcc, String subject, String content,
827            String signature) {
828        String toText = mToView.getText().toString();
829        if (to == null) {
830            assertEquals(0, toText.length());
831        } else {
832            assertEquals(to, toText);
833            TestUtils.assertViewVisible(mToView);
834        }
835
836        String ccText = mCcView.getText().toString();
837        if (cc == null) {
838            assertEquals(0, ccText.length());
839        } else {
840            assertEquals(cc, ccText);
841            TestUtils.assertViewVisible(mCcView);
842        }
843
844        String bccText = mBccView.getText().toString();
845        if (bcc == null) {
846            assertEquals(0, bccText.length());
847        } else {
848            assertEquals(bcc, bccText);
849            TestUtils.assertViewVisible(mBccView);
850        }
851
852        String subjectText = mSubjectView.getText().toString();
853        if (subject == null) {
854            assertEquals(0, subjectText.length());
855        } else {
856            assertEquals(subject, subjectText);
857        }
858
859        String contentText = mMessageView.getText().toString();
860        if (content == null && signature == null) {
861            assertEquals(0, contentText.length());
862        } else {
863            if (content == null) content = "";
864            if (signature != null) {
865                int textLength = content.length();
866                if (textLength == 0 || content.charAt(textLength - 1) != '\n') {
867                    content += "\n";
868                }
869                content += signature;
870            }
871            assertEquals(content, contentText);
872        }
873    }
874
875    /**
876     * Helper method to verify which field has the focus
877     * @param focused The view that should be focused (all others should not have focus)
878     */
879    private void checkFocused(View focused) {
880        assertEquals(focused == mToView, mToView.isFocused());
881        assertEquals(focused == mSubjectView, mSubjectView.isFocused());
882        assertEquals(focused == mMessageView, mMessageView.isFocused());
883    }
884
885    /**
886     * Helper used when running multiple calls to processSourceMessage within a test method.
887     * Simply clears out the views, so that we get fresh data and not appended data.
888     *
889     * Must call from UI thread.
890     */
891    private void resetViews() {
892        mToView.setText(null);
893        mSubjectView.setText(null);
894        mMessageView.setText(null);
895    }
896
897    /**
898     * Build a test message that can be used as input to processSourceMessage
899     *
900     * @param to Recipient(s) of the message
901     * @param sender Sender(s) of the message
902     * @param subject Subject of the message
903     * @param content Content of the message
904     * @return a complete Message object
905     */
906    private Message buildTestMessage(String to, String sender, String subject, String content) {
907        Message message = new Message();
908
909        if (to != null) {
910            message.mTo = Address.parseAndPack(to);
911        }
912
913        if (sender != null) {
914            Address[] addresses = Address.parse(sender);
915            assertTrue("from address", addresses.length > 0);
916            message.mFrom = addresses[0].pack();
917        }
918
919        message.mSubject = subject;
920
921        if (content != null) {
922            message.mText = content;
923        }
924
925        return message;
926    }
927
928    /**
929     * Check AddressTextView email address validation.
930     */
931    @UiThreadTest
932    public void testAddressTextView() {
933        MessageCompose messageCompose = getActivity();
934
935        mToView.setValidator(new EmailAddressValidator());
936        mToView.setText("foo");
937        mToView.performValidation();
938
939        // address is validated as errorneous
940        assertFalse(messageCompose.isAddressAllValid());
941
942        // the wrong address is preserved by validation
943        assertEquals("foo", mToView.getText().toString());
944
945        mToView.setText("a@b.c");
946        mToView.performValidation();
947
948        // address is validated as correct
949        assertTrue(messageCompose.isAddressAllValid());
950
951        mToView.setText("a@b.c, foo");
952        mToView.performValidation();
953
954        assertFalse(messageCompose.isAddressAllValid());
955        assertEquals("a@b.c, foo", mToView.getText().toString());
956    }
957
958    /**
959     * Check message and selection with/without signature.
960     */
961    public void testSetInitialComposeTextAndSelection() throws MessagingException, Throwable {
962        final Message msg = buildTestMessage(null, null, null, BODY);
963        final Intent intent = new Intent(ACTION_EDIT_DRAFT);
964        final Account account = new Account();
965        final MessageCompose a = getActivity();
966        a.setIntent(intent);
967
968        runTestOnUiThread(new Runnable() {
969            public void run() {
970                resetViews();
971                a.setInitialComposeText(BODY, SIGNATURE);
972                checkFields(null, null, null, null, BODY, SIGNATURE);
973                a.setMessageContentSelection(SIGNATURE);
974                assertEquals(BODY.length(), mMessageView.getSelectionStart());
975            }
976        });
977
978        runTestOnUiThread(new Runnable() {
979            public void run() {
980                resetViews();
981                a.setInitialComposeText(BODY, null);
982                checkFields(null, null, null, null, BODY, null);
983                a.setMessageContentSelection(null);
984                assertEquals(BODY.length(), mMessageView.getSelectionStart());
985            }
986        });
987
988        runTestOnUiThread(new Runnable() {
989            public void run() {
990                resetViews();
991                final String body2 = BODY + "\n\na\n\n";
992                a.setInitialComposeText(body2, SIGNATURE);
993                checkFields(null, null, null, null, body2, SIGNATURE);
994                a.setMessageContentSelection(SIGNATURE);
995                assertEquals(BODY.length() + 3, mMessageView.getSelectionStart());
996            }
997        });
998
999    }
1000
1001    private static int sAttachmentId = 1;
1002    private Attachment makeAttachment(String filename) {
1003        Attachment a = new Attachment();
1004        a.mId = sAttachmentId++;
1005        a.mFileName = filename;
1006        return a;
1007    }
1008
1009    @SmallTest
1010    public void testSourceAttachmentsProcessing() {
1011        // Attachments currently in the draft.
1012        ArrayList<Attachment> currentAttachments = Lists.newArrayList(
1013                makeAttachment("a.png"), makeAttachment("b.png"));
1014
1015        // Attachments in the message being forwarded.
1016        Attachment c = makeAttachment("c.png");
1017        Attachment d = makeAttachment("d.png");
1018        ArrayList<Attachment> sourceAttachments = Lists.newArrayList(c, d);
1019
1020        // Ensure the source attachments gets added.
1021        final MessageCompose a = getActivity();
1022        a.processSourceMessageAttachments(currentAttachments, sourceAttachments, true /*include*/);
1023
1024        assertEquals(4, currentAttachments.size());
1025        assertTrue(currentAttachments.contains(c));
1026        assertTrue(currentAttachments.contains(d));
1027
1028        // Now ensure they can be removed (e.g. in the case of switching from forward to reply).
1029        a.processSourceMessageAttachments(currentAttachments, sourceAttachments, false /*include*/);
1030        assertEquals(2, currentAttachments.size());
1031        assertFalse(currentAttachments.contains(c));
1032        assertFalse(currentAttachments.contains(d));
1033    }
1034}
1035