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