AccountSetupBasics.java revision ecb1af804144689d4ead96a247b565f9b4eb8160
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.setup;
18
19import com.android.email.AccountBackupRestore;
20import com.android.email.Email;
21import com.android.email.EmailAddressValidator;
22import com.android.email.R;
23import com.android.email.Utility;
24import com.android.email.VendorPolicyLoader;
25import com.android.email.activity.Debug;
26import com.android.email.activity.MessageList;
27import com.android.email.provider.EmailContent;
28import com.android.email.provider.EmailContent.Account;
29import com.android.email.provider.EmailContent.Mailbox;
30
31import android.app.Activity;
32import android.app.AlertDialog;
33import android.app.Dialog;
34import android.content.Context;
35import android.content.DialogInterface;
36import android.content.Intent;
37import android.content.res.XmlResourceParser;
38import android.database.Cursor;
39import android.os.Bundle;
40import android.text.Editable;
41import android.text.TextWatcher;
42import android.util.Log;
43import android.view.View;
44import android.view.View.OnClickListener;
45import android.widget.Button;
46import android.widget.CheckBox;
47import android.widget.EditText;
48import android.widget.TextView;
49import android.widget.Toast;
50
51import java.io.Serializable;
52import java.net.URI;
53import java.net.URISyntaxException;
54
55/**
56 * Prompts the user for the email address and password. Also prompts for
57 * "Use this account as default" if this is the 2nd+ account being set up.
58 * Attempts to lookup default settings for the domain the user specified. If the
59 * domain is known the settings are handed off to the AccountSetupCheckSettings
60 * activity. If no settings are found the settings are handed off to the
61 * AccountSetupAccountType activity.
62 */
63public class AccountSetupBasics extends Activity
64        implements OnClickListener, TextWatcher {
65    private final static boolean ENTER_DEBUG_SCREEN = true;
66
67    private final static String EXTRA_ACCOUNT = "com.android.email.AccountSetupBasics.account";
68    public final static String EXTRA_EAS_FLOW = "com.android.email.extra.eas_flow";
69
70    // Action asking us to return to our original caller (i.e. finish)
71    private static final String ACTION_RETURN_TO_CALLER =
72        "com.android.email.AccountSetupBasics.return";
73    // Action asking us to restart the task from the Welcome activity (which will figure out the
74    // right place to go) and finish
75    private static final String ACTION_START_AT_MESSAGE_LIST =
76        "com.android.email.AccountSetupBasics.messageList";
77
78    private final static String EXTRA_USERNAME = "com.android.email.AccountSetupBasics.username";
79    private final static String EXTRA_PASSWORD = "com.android.email.AccountSetupBasics.password";
80
81    private final static int DIALOG_NOTE = 1;
82    private final static int DIALOG_DUPLICATE_ACCOUNT = 2;
83
84    private final static String STATE_KEY_PROVIDER =
85        "com.android.email.AccountSetupBasics.provider";
86
87    // NOTE: If you change this value, confirm that the new interval exists in arrays.xml
88    private final static int DEFAULT_ACCOUNT_CHECK_INTERVAL = 15;
89
90    private EditText mEmailView;
91    private EditText mPasswordView;
92    private CheckBox mDefaultView;
93    private Button mNextButton;
94    private Button mManualSetupButton;
95    private EmailContent.Account mAccount;
96    private Provider mProvider;
97    private boolean mEasFlowMode;
98    private String mDuplicateAccountName;
99
100    private EmailAddressValidator mEmailValidator = new EmailAddressValidator();
101
102    public static void actionNewAccount(Activity fromActivity) {
103        Intent i = new Intent(fromActivity, AccountSetupBasics.class);
104        fromActivity.startActivity(i);
105    }
106
107    public static void actionNewAccountWithCredentials(Activity fromActivity,
108            String username, String password, boolean easFlow) {
109        Intent i = new Intent(fromActivity, AccountSetupBasics.class);
110        i.putExtra(EXTRA_USERNAME, username);
111        i.putExtra(EXTRA_PASSWORD, password);
112        i.putExtra(EXTRA_EAS_FLOW, easFlow);
113        fromActivity.startActivity(i);
114    }
115
116    /**
117     * This creates an intent that can be used to start a self-contained account creation flow
118     * for exchange accounts.
119     */
120    public static Intent actionSetupExchangeIntent(Context context) {
121        Intent i = new Intent(context, AccountSetupBasics.class);
122        i.putExtra(EXTRA_EAS_FLOW, true);
123        return i;
124    }
125
126    public static void actionAccountCreateFinishedEas(Activity fromActivity) {
127        Intent i= new Intent(fromActivity, AccountSetupBasics.class);
128        // If we're in the "eas flow" (from AccountManager), we want to return to the caller
129        // (in the settings app)
130        i.putExtra(AccountSetupBasics.ACTION_RETURN_TO_CALLER, true);
131        i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
132        fromActivity.startActivity(i);
133    }
134
135    public static void actionAccountCreateFinished(Activity fromActivity, long accountId) {
136        Intent i= new Intent(fromActivity, AccountSetupBasics.class);
137        // If we're not in the "eas flow" (from AccountManager), we want to show the message list
138        // for the new inbox
139        i.putExtra(AccountSetupBasics.ACTION_START_AT_MESSAGE_LIST, accountId);
140        i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
141        fromActivity.startActivity(i);
142    }
143
144    @Override
145    public void onCreate(Bundle savedInstanceState) {
146        super.onCreate(savedInstanceState);
147
148        Intent intent = getIntent();
149        if (intent.getBooleanExtra(ACTION_RETURN_TO_CALLER, false)) {
150            // Return to the caller who initiated account creation
151            finish();
152            return;
153        } else {
154            long accountId = intent.getLongExtra(ACTION_START_AT_MESSAGE_LIST, -1);
155            if (accountId >= 0) {
156                // Show the message list for the new account
157                MessageList.actionHandleAccount(this, accountId, Mailbox.TYPE_INBOX);
158                finish();
159                return;
160            }
161        }
162
163        setContentView(R.layout.account_setup_basics);
164
165        mEmailView = (EditText)findViewById(R.id.account_email);
166        mPasswordView = (EditText)findViewById(R.id.account_password);
167        mDefaultView = (CheckBox)findViewById(R.id.account_default);
168        mNextButton = (Button)findViewById(R.id.next);
169        mManualSetupButton = (Button)findViewById(R.id.manual_setup);
170
171        mNextButton.setOnClickListener(this);
172        mManualSetupButton.setOnClickListener(this);
173
174        mEmailView.addTextChangedListener(this);
175        mPasswordView.addTextChangedListener(this);
176
177        // Find out how many accounts we have, and if there one or more, then we have a choice
178        // about being default or not.
179        Cursor c = null;
180        try {
181            c = getContentResolver().query(
182                    EmailContent.Account.CONTENT_URI,
183                    EmailContent.Account.ID_PROJECTION,
184                    null, null, null);
185            if (c.getCount() > 0) {
186                mDefaultView.setVisibility(View.VISIBLE);
187            }
188        } finally {
189            if (c != null) {
190                c.close();
191            }
192        }
193
194        mEasFlowMode = intent.getBooleanExtra(EXTRA_EAS_FLOW, false);
195        if (mEasFlowMode) {
196            // No need for manual button -> next is appropriate
197            mManualSetupButton.setVisibility(View.GONE);
198            // Swap welcome text for EAS-specific text
199            TextView welcomeView = (TextView) findViewById(R.id.instructions);
200            final boolean alternateStrings =
201                    VendorPolicyLoader.getInstance(this).useAlternateExchangeStrings();
202            setTitle(alternateStrings
203                    ? R.string.account_setup_basics_exchange_title_alternate
204                    : R.string.account_setup_basics_exchange_title);
205            welcomeView.setText(alternateStrings
206                    ? R.string.accounts_welcome_exchange_alternate
207                    : R.string.accounts_welcome_exchange);
208        }
209
210        if (intent.hasExtra(EXTRA_USERNAME)) {
211            mEmailView.setText(intent.getStringExtra(EXTRA_USERNAME));
212        }
213        if (intent.hasExtra(EXTRA_PASSWORD)) {
214            mPasswordView.setText(intent.getStringExtra(EXTRA_PASSWORD));
215        }
216
217        if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
218            mAccount = (EmailContent.Account)savedInstanceState.getParcelable(EXTRA_ACCOUNT);
219        }
220
221        if (savedInstanceState != null && savedInstanceState.containsKey(STATE_KEY_PROVIDER)) {
222            mProvider = (Provider)savedInstanceState.getSerializable(STATE_KEY_PROVIDER);
223        }
224    }
225
226    @Override
227    public void onResume() {
228        super.onResume();
229        validateFields();
230    }
231
232    @Override
233    public void onSaveInstanceState(Bundle outState) {
234        super.onSaveInstanceState(outState);
235        outState.putParcelable(EXTRA_ACCOUNT, mAccount);
236        if (mProvider != null) {
237            outState.putSerializable(STATE_KEY_PROVIDER, mProvider);
238        }
239    }
240
241    public void afterTextChanged(Editable s) {
242        validateFields();
243    }
244
245    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
246    }
247
248    public void onTextChanged(CharSequence s, int start, int before, int count) {
249    }
250
251    private void validateFields() {
252        boolean valid = Utility.requiredFieldValid(mEmailView)
253                && Utility.requiredFieldValid(mPasswordView)
254                && mEmailValidator.isValid(mEmailView.getText().toString().trim());
255        mNextButton.setEnabled(valid);
256        mManualSetupButton.setEnabled(valid);
257        /*
258         * Dim the next button's icon to 50% if the button is disabled.
259         * TODO this can probably be done with a stateful drawable. Check into it.
260         * android:state_enabled
261         */
262        Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
263    }
264
265    private String getOwnerName() {
266        String name = null;
267/* TODO figure out another way to get the owner name
268        String projection[] = {
269            ContactMethods.NAME
270        };
271        Cursor c = getContentResolver().query(
272                Uri.withAppendedPath(Contacts.People.CONTENT_URI, "owner"), projection, null, null,
273                null);
274        if (c != null) {
275            if (c.moveToFirst()) {
276                name = c.getString(0);
277            }
278            c.close();
279        }
280*/
281
282        if (name == null || name.length() == 0) {
283            long defaultId = Account.getDefaultAccountId(this);
284            if (defaultId != -1) {
285                Account account = Account.restoreAccountWithId(this, defaultId);
286                if (account != null) {
287                    name = account.getSenderName();
288                }
289            }
290        }
291        return name;
292    }
293
294    @Override
295    public Dialog onCreateDialog(int id) {
296        if (id == DIALOG_NOTE) {
297            if (mProvider != null && mProvider.note != null) {
298                return new AlertDialog.Builder(this)
299                .setIcon(android.R.drawable.ic_dialog_alert)
300                .setTitle(android.R.string.dialog_alert_title)
301                .setMessage(mProvider.note)
302                .setPositiveButton(
303                        getString(R.string.okay_action),
304                        new DialogInterface.OnClickListener() {
305                            public void onClick(DialogInterface dialog, int which) {
306                                finishAutoSetup();
307                            }
308                        })
309                        .setNegativeButton(
310                                getString(R.string.cancel_action),
311                                null)
312                                .create();
313            }
314        } else if (id == DIALOG_DUPLICATE_ACCOUNT) {
315            return new AlertDialog.Builder(this)
316            .setIcon(android.R.drawable.ic_dialog_alert)
317            .setTitle(R.string.account_duplicate_dlg_title)
318            .setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
319                    mDuplicateAccountName))
320                    .setPositiveButton(R.string.okay_action,
321                            new DialogInterface.OnClickListener() {
322                        public void onClick(DialogInterface dialog, int which) {
323                            dismissDialog(DIALOG_DUPLICATE_ACCOUNT);
324                        }
325                    })
326                    .create();
327        }
328        return null;
329    }
330
331    /**
332     * Update a cached dialog with current values (e.g. account name)
333     */
334    @Override
335    public void onPrepareDialog(int id, Dialog dialog) {
336        switch (id) {
337            case DIALOG_NOTE:
338                if (mProvider != null && mProvider.note != null) {
339                    AlertDialog alert = (AlertDialog) dialog;
340                    alert.setMessage(mProvider.note);
341                }
342                break;
343            case DIALOG_DUPLICATE_ACCOUNT:
344                if (mDuplicateAccountName != null) {
345                    AlertDialog alert = (AlertDialog) dialog;
346                    alert.setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
347                            mDuplicateAccountName));
348                }
349                break;
350        }
351    }
352
353    private void finishAutoSetup() {
354        String email = mEmailView.getText().toString().trim();
355        String password = mPasswordView.getText().toString().trim();
356        String[] emailParts = email.split("@");
357        String user = emailParts[0];
358        String domain = emailParts[1];
359        URI incomingUri = null;
360        URI outgoingUri = null;
361        try {
362            String incomingUsername = mProvider.incomingUsernameTemplate;
363            incomingUsername = incomingUsername.replaceAll("\\$email", email);
364            incomingUsername = incomingUsername.replaceAll("\\$user", user);
365            incomingUsername = incomingUsername.replaceAll("\\$domain", domain);
366
367            URI incomingUriTemplate = mProvider.incomingUriTemplate;
368            incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":"
369                    + password, incomingUriTemplate.getHost(), incomingUriTemplate.getPort(),
370                    incomingUriTemplate.getPath(), null, null);
371
372            String outgoingUsername = mProvider.outgoingUsernameTemplate;
373            outgoingUsername = outgoingUsername.replaceAll("\\$email", email);
374            outgoingUsername = outgoingUsername.replaceAll("\\$user", user);
375            outgoingUsername = outgoingUsername.replaceAll("\\$domain", domain);
376
377            URI outgoingUriTemplate = mProvider.outgoingUriTemplate;
378            outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":"
379                    + password, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(),
380                    outgoingUriTemplate.getPath(), null, null);
381
382            // Stop here if the login credentials duplicate an existing account
383            mDuplicateAccountName = Utility.findDuplicateAccount(this, -1,
384                    incomingUri.getHost(), incomingUsername);
385            if (mDuplicateAccountName != null) {
386                this.showDialog(DIALOG_DUPLICATE_ACCOUNT);
387                return;
388            }
389
390        } catch (URISyntaxException use) {
391            /*
392             * If there is some problem with the URI we give up and go on to
393             * manual setup.
394             */
395            onManualSetup();
396            return;
397        }
398
399        mAccount = new EmailContent.Account();
400        mAccount.setSenderName(getOwnerName());
401        mAccount.setEmailAddress(email);
402        mAccount.setStoreUri(this, incomingUri.toString());
403        mAccount.setSenderUri(this, outgoingUri.toString());
404/* TODO figure out the best way to implement this concept
405        mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
406        mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
407        mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox));
408        mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
409*/
410        if (incomingUri.toString().startsWith("imap")) {
411            // Delete policy must be set explicitly, because IMAP does not provide a UI selection
412            // for it. This logic needs to be followed in the auto setup flow as well.
413            mAccount.setDeletePolicy(EmailContent.Account.DELETE_POLICY_ON_DELETE);
414        }
415        mAccount.setSyncInterval(DEFAULT_ACCOUNT_CHECK_INTERVAL);
416        AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, true);
417    }
418
419    private void onNext() {
420        // If this is EAS flow, don't try to find a provider for the domain!
421        if (!mEasFlowMode) {
422            String email = mEmailView.getText().toString().trim();
423            String[] emailParts = email.split("@");
424            String domain = emailParts[1].trim();
425            mProvider = findProviderForDomain(domain);
426            if (mProvider != null) {
427                if (mProvider.note != null) {
428                    showDialog(DIALOG_NOTE);
429                } else {
430                    finishAutoSetup();
431                }
432                return;
433            }
434        }
435        // Can't use auto setup
436        onManualSetup();
437    }
438
439    /**
440     * This is used in automatic setup mode to jump directly down to the names screen.
441     *
442     * NOTE:  With this organization, it is *not* possible to auto-create an exchange account,
443     * because certain necessary actions happen during AccountSetupOptions (which we are
444     * skipping here).
445     */
446    @Override
447    public void onActivityResult(int requestCode, int resultCode, Intent data) {
448        if (resultCode == RESULT_OK) {
449            String email = mAccount.getEmailAddress();
450            boolean isDefault = mDefaultView.isChecked();
451            mAccount.setDisplayName(email);
452            mAccount.setDefaultAccount(isDefault);
453            // At this point we write the Account object to the DB for the first time.
454            // From now on we'll only pass the accountId around.
455            mAccount.save(this);
456            // Update the backup (side copy) of the accounts
457            AccountBackupRestore.backupAccounts(this);
458            Email.setServicesEnabled(this);
459            AccountSetupNames.actionSetNames(this, mAccount.mId, false);
460            finish();
461        }
462    }
463
464    private void onManualSetup() {
465        String email = mEmailView.getText().toString().trim();
466        String password = mPasswordView.getText().toString().trim();
467        String[] emailParts = email.split("@");
468        String user = emailParts[0].trim();
469        String domain = emailParts[1].trim();
470
471        // Alternate entry to the debug options screen (for devices without a physical keyboard:
472        //  Username: d@d.d
473        //  Password: debug
474        if (ENTER_DEBUG_SCREEN && "d@d.d".equals(email) && "debug".equals(password)) {
475            mEmailView.setText("");
476            mPasswordView.setText("");
477            startActivity(new Intent(this, Debug.class));
478            return;
479        }
480
481        mAccount = new EmailContent.Account();
482        mAccount.setSenderName(getOwnerName());
483        mAccount.setEmailAddress(email);
484        try {
485            URI uri = new URI("placeholder", user + ":" + password, domain, -1, null, null, null);
486            mAccount.setStoreUri(this, uri.toString());
487            mAccount.setSenderUri(this, uri.toString());
488        } catch (URISyntaxException use) {
489            // If we can't set up the URL, don't continue - account setup pages will fail too
490            Toast.makeText(this, R.string.account_setup_username_password_toast, Toast.LENGTH_LONG)
491                    .show();
492            mAccount = null;
493            return;
494        }
495/* TODO figure out the best way to implement this concept
496        mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
497        mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
498        mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox));
499        mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
500*/
501        mAccount.setSyncInterval(DEFAULT_ACCOUNT_CHECK_INTERVAL);
502
503        AccountSetupAccountType.actionSelectAccountType(this, mAccount, mDefaultView.isChecked(),
504                mEasFlowMode);
505    }
506
507    public void onClick(View v) {
508        switch (v.getId()) {
509            case R.id.next:
510                onNext();
511                break;
512            case R.id.manual_setup:
513                onManualSetup();
514                break;
515        }
516    }
517
518    /**
519     * Attempts to get the given attribute as a String resource first, and if it fails
520     * returns the attribute as a simple String value.
521     * @param xml
522     * @param name
523     * @return the requested resource
524     */
525    private String getXmlAttribute(XmlResourceParser xml, String name) {
526        int resId = xml.getAttributeResourceValue(null, name, 0);
527        if (resId == 0) {
528            return xml.getAttributeValue(null, name);
529        }
530        else {
531            return getString(resId);
532        }
533    }
534
535    /**
536     * Search the list of known Email providers looking for one that matches the user's email
537     * domain.  We check for vendor supplied values first, then we look in providers_product.xml
538     * first, finally by the entries in platform providers.xml.  This provides a nominal override
539     * capability.
540     *
541     * A match is defined as any provider entry for which the "domain" attribute matches.
542     *
543     * @param domain The domain portion of the user's email address
544     * @return suitable Provider definition, or null if no match found
545     */
546    private Provider findProviderForDomain(String domain) {
547        Provider p = VendorPolicyLoader.getInstance(this).findProviderForDomain(domain);
548        if (p == null) {
549            p = findProviderForDomain(domain, R.xml.providers_product);
550        }
551        if (p == null) {
552            p = findProviderForDomain(domain, R.xml.providers);
553        }
554        return p;
555    }
556
557    /**
558     * Search a single resource containing known Email provider definitions.
559     *
560     * @param domain The domain portion of the user's email address
561     * @param resourceId Id of the provider resource to scan
562     * @return suitable Provider definition, or null if no match found
563     */
564    private Provider findProviderForDomain(String domain, int resourceId) {
565        try {
566            XmlResourceParser xml = getResources().getXml(resourceId);
567            int xmlEventType;
568            Provider provider = null;
569            while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
570                if (xmlEventType == XmlResourceParser.START_TAG
571                        && "provider".equals(xml.getName())
572                        && domain.equalsIgnoreCase(getXmlAttribute(xml, "domain"))) {
573                    provider = new Provider();
574                    provider.id = getXmlAttribute(xml, "id");
575                    provider.label = getXmlAttribute(xml, "label");
576                    provider.domain = getXmlAttribute(xml, "domain");
577                    provider.note = getXmlAttribute(xml, "note");
578                }
579                else if (xmlEventType == XmlResourceParser.START_TAG
580                        && "incoming".equals(xml.getName())
581                        && provider != null) {
582                    provider.incomingUriTemplate = new URI(getXmlAttribute(xml, "uri"));
583                    provider.incomingUsernameTemplate = getXmlAttribute(xml, "username");
584                }
585                else if (xmlEventType == XmlResourceParser.START_TAG
586                        && "outgoing".equals(xml.getName())
587                        && provider != null) {
588                    provider.outgoingUriTemplate = new URI(getXmlAttribute(xml, "uri"));
589                    provider.outgoingUsernameTemplate = getXmlAttribute(xml, "username");
590                }
591                else if (xmlEventType == XmlResourceParser.END_TAG
592                        && "provider".equals(xml.getName())
593                        && provider != null) {
594                    return provider;
595                }
596            }
597        }
598        catch (Exception e) {
599            Log.e(Email.LOG_TAG, "Error while trying to load provider settings.", e);
600        }
601        return null;
602    }
603
604    public static class Provider implements Serializable {
605        private static final long serialVersionUID = 8511656164616538989L;
606
607        public String id;
608        public String label;
609        public String domain;
610        public URI incomingUriTemplate;
611        public String incomingUsernameTemplate;
612        public URI outgoingUriTemplate;
613        public String outgoingUsernameTemplate;
614        public String note;
615    }
616}
617