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