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