AccountSetupExchange.java revision d6d874f8c6ce2580ef9ec2406fe411af45b2d92d
1/*
2 * Copyright (C) 2009 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.ExchangeUtils;
21import com.android.email.R;
22import com.android.email.Utility;
23import com.android.email.SecurityPolicy.PolicySet;
24import com.android.email.provider.EmailContent;
25import com.android.email.provider.EmailContent.Account;
26import com.android.email.provider.EmailContent.HostAuth;
27import com.android.email.service.EmailServiceProxy;
28import com.android.exchange.SyncManager;
29
30import android.app.Activity;
31import android.app.AlertDialog;
32import android.app.Dialog;
33import android.content.DialogInterface;
34import android.content.Intent;
35import android.os.Bundle;
36import android.os.Parcelable;
37import android.os.RemoteException;
38import android.text.Editable;
39import android.text.TextWatcher;
40import android.view.View;
41import android.view.View.OnClickListener;
42import android.widget.Button;
43import android.widget.CheckBox;
44import android.widget.CompoundButton;
45import android.widget.EditText;
46import android.widget.TextView;
47import android.widget.CompoundButton.OnCheckedChangeListener;
48
49import java.io.IOException;
50import java.net.URI;
51import java.net.URISyntaxException;
52
53/**
54 * Provides generic setup for Exchange accounts.  The following fields are supported:
55 *
56 *  Email Address   (from previous setup screen)
57 *  Server
58 *  Domain
59 *  Requires SSL?
60 *  User (login)
61 *  Password
62 *
63 * There are two primary paths through this activity:
64 *   Edit existing:
65 *     Load existing values from account into fields
66 *     When user clicks 'next':
67 *       Confirm not a duplicate account
68 *       Try new values (check settings)
69 *       If new values are OK:
70 *         Write new values (save to provider)
71 *         finish() (pop to previous)
72 *
73 *   Creating New:
74 *     Try Auto-discover to get details from server
75 *     If Auto-discover reports an authentication failure:
76 *       finish() (pop to previous, to re-enter username & password)
77 *     If Auto-discover succeeds:
78 *       write server's account details into account
79 *     Load values from account into fields
80 *     Confirm not a duplicate account
81 *     Try new values (check settings)
82 *     If new values are OK:
83 *       Write new values (save to provider)
84 *       Proceed to options screen
85 *       finish() (removes self from back stack)
86 *
87 * NOTE: The manifest for this activity has it ignore config changes, because
88 * we don't want to restart on every orientation - this would launch autodiscover again.
89 * Do not attempt to define orientation-specific resources, they won't be loaded.
90 */
91public class AccountSetupExchange extends Activity implements OnClickListener,
92        OnCheckedChangeListener {
93    /*package*/ static final String EXTRA_ACCOUNT = "account";
94    private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
95    private static final String EXTRA_EAS_FLOW = "easFlow";
96    /*package*/ static final String EXTRA_DISABLE_AUTO_DISCOVER = "disableAutoDiscover";
97
98    private final static int DIALOG_DUPLICATE_ACCOUNT = 1;
99
100    private EditText mUsernameView;
101    private EditText mPasswordView;
102    private EditText mServerView;
103    private CheckBox mSslSecurityView;
104    private CheckBox mTrustCertificatesView;
105
106    private Button mNextButton;
107    private Account mAccount;
108    private boolean mMakeDefault;
109    private String mCacheLoginCredential;
110    private String mDuplicateAccountName;
111
112    public static void actionIncomingSettings(Activity fromActivity, Account account,
113            boolean makeDefault, boolean easFlowMode, boolean allowAutoDiscover) {
114        Intent i = new Intent(fromActivity, AccountSetupExchange.class);
115        i.putExtra(EXTRA_ACCOUNT, account);
116        i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
117        i.putExtra(EXTRA_EAS_FLOW, easFlowMode);
118        if (!allowAutoDiscover) {
119            i.putExtra(EXTRA_DISABLE_AUTO_DISCOVER, true);
120        }
121        fromActivity.startActivity(i);
122    }
123
124    public static void actionEditIncomingSettings(Activity fromActivity, Account account)
125            {
126        Intent i = new Intent(fromActivity, AccountSetupExchange.class);
127        i.setAction(Intent.ACTION_EDIT);
128        i.putExtra(EXTRA_ACCOUNT, account);
129        fromActivity.startActivity(i);
130    }
131
132    /**
133     * For now, we'll simply replicate outgoing, for the purpose of satisfying the
134     * account settings flow.
135     */
136    public static void actionEditOutgoingSettings(Activity fromActivity, Account account)
137            {
138        Intent i = new Intent(fromActivity, AccountSetupExchange.class);
139        i.setAction(Intent.ACTION_EDIT);
140        i.putExtra(EXTRA_ACCOUNT, account);
141        fromActivity.startActivity(i);
142    }
143
144    @Override
145    public void onCreate(Bundle savedInstanceState) {
146        super.onCreate(savedInstanceState);
147        setContentView(R.layout.account_setup_exchange);
148
149        mUsernameView = (EditText) findViewById(R.id.account_username);
150        mPasswordView = (EditText) findViewById(R.id.account_password);
151        mServerView = (EditText) findViewById(R.id.account_server);
152        mSslSecurityView = (CheckBox) findViewById(R.id.account_ssl);
153        mSslSecurityView.setOnCheckedChangeListener(this);
154        mTrustCertificatesView = (CheckBox) findViewById(R.id.account_trust_certificates);
155
156        mNextButton = (Button)findViewById(R.id.next);
157        mNextButton.setOnClickListener(this);
158
159        /*
160         * Calls validateFields() which enables or disables the Next button
161         * based on the fields' validity.
162         */
163        TextWatcher validationTextWatcher = new TextWatcher() {
164            public void afterTextChanged(Editable s) {
165                validateFields();
166            }
167
168            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
169            }
170
171            public void onTextChanged(CharSequence s, int start, int before, int count) {
172            }
173        };
174        mUsernameView.addTextChangedListener(validationTextWatcher);
175        mPasswordView.addTextChangedListener(validationTextWatcher);
176        mServerView.addTextChangedListener(validationTextWatcher);
177
178        Intent intent = getIntent();
179        mAccount = (EmailContent.Account) intent.getParcelableExtra(EXTRA_ACCOUNT);
180        mMakeDefault = intent.getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
181
182        /*
183         * If we're being reloaded we override the original account with the one
184         * we saved
185         */
186        if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
187            mAccount = (EmailContent.Account) savedInstanceState.getParcelable(EXTRA_ACCOUNT);
188        }
189
190        loadFields(mAccount);
191        validateFields();
192
193        // If we've got a username and password and we're NOT editing, try autodiscover
194        String username = mAccount.mHostAuthRecv.mLogin;
195        String password = mAccount.mHostAuthRecv.mPassword;
196        if (username != null && password != null &&
197                !Intent.ACTION_EDIT.equals(intent.getAction())) {
198            // NOTE: Disabling AutoDiscover is only used in unit tests
199            boolean disableAutoDiscover =
200                intent.getBooleanExtra(EXTRA_DISABLE_AUTO_DISCOVER, false);
201            if (!disableAutoDiscover) {
202                AccountSetupCheckSettings
203                    .actionAutoDiscover(this, mAccount, mAccount.mEmailAddress, password);
204            }
205        }
206
207        //EXCHANGE-REMOVE-SECTION-START
208        // Show device ID
209        try {
210            ((TextView) findViewById(R.id.device_id)).setText(SyncManager.getDeviceId(this));
211        } catch (IOException ignore) {
212            // There's nothing we can do here...
213        }
214        //EXCHANGE-REMOVE-SECTION-END
215    }
216
217    @Override
218    public void onSaveInstanceState(Bundle outState) {
219        super.onSaveInstanceState(outState);
220        outState.putParcelable(EXTRA_ACCOUNT, mAccount);
221    }
222
223    private boolean usernameFieldValid(EditText usernameView) {
224        return Utility.requiredFieldValid(usernameView) &&
225            !usernameView.getText().toString().equals("\\");
226    }
227
228    /**
229     * Prepare a cached dialog with current values (e.g. account name)
230     */
231    @Override
232    public Dialog onCreateDialog(int id) {
233        switch (id) {
234            case DIALOG_DUPLICATE_ACCOUNT:
235                return new AlertDialog.Builder(this)
236                    .setIcon(android.R.drawable.ic_dialog_alert)
237                    .setTitle(R.string.account_duplicate_dlg_title)
238                    .setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
239                            mDuplicateAccountName))
240                    .setPositiveButton(R.string.okay_action,
241                            new DialogInterface.OnClickListener() {
242                        public void onClick(DialogInterface dialog, int which) {
243                            dismissDialog(DIALOG_DUPLICATE_ACCOUNT);
244                        }
245                    })
246                    .create();
247        }
248        return null;
249    }
250
251    /**
252     * Update a cached dialog with current values (e.g. account name)
253     */
254    @Override
255    public void onPrepareDialog(int id, Dialog dialog) {
256        switch (id) {
257            case DIALOG_DUPLICATE_ACCOUNT:
258                if (mDuplicateAccountName != null) {
259                    AlertDialog alert = (AlertDialog) dialog;
260                    alert.setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
261                            mDuplicateAccountName));
262                }
263                break;
264        }
265    }
266
267    /**
268     * Copy mAccount's values into UI fields
269     */
270    /* package */ void loadFields(Account account) {
271        HostAuth hostAuth = account.mHostAuthRecv;
272
273        String userName = hostAuth.mLogin;
274        if (userName != null) {
275            // Add a backslash to the start of the username, but only if the username has no
276            // backslash in it.
277            if (userName.indexOf('\\') < 0) {
278                userName = "\\" + userName;
279            }
280            mUsernameView.setText(userName);
281        }
282
283        if (hostAuth.mPassword != null) {
284            mPasswordView.setText(hostAuth.mPassword);
285        }
286
287        String protocol = hostAuth.mProtocol;
288        if (protocol == null || !protocol.startsWith("eas")) {
289            throw new Error("Unknown account type: " + account.getStoreUri(this));
290        }
291
292        if (hostAuth.mAddress != null) {
293            mServerView.setText(hostAuth.mAddress);
294        }
295
296        boolean ssl = 0 != (hostAuth.mFlags & HostAuth.FLAG_SSL);
297        boolean trustCertificates = 0 != (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES);
298        mSslSecurityView.setChecked(ssl);
299        mTrustCertificatesView.setChecked(trustCertificates);
300        mTrustCertificatesView.setVisibility(ssl ? View.VISIBLE : View.GONE);
301    }
302
303    /**
304     * Check the values in the fields and decide if it makes sense to enable the "next" button
305     * NOTE:  Does it make sense to extract & combine with similar code in AccountSetupIncoming?
306     * @return true if all fields are valid, false if fields are incomplete
307     */
308    private boolean validateFields() {
309        boolean enabled = usernameFieldValid(mUsernameView)
310                && Utility.requiredFieldValid(mPasswordView)
311                && Utility.requiredFieldValid(mServerView);
312        if (enabled) {
313            try {
314                URI uri = getUri();
315            } catch (URISyntaxException use) {
316                enabled = false;
317            }
318        }
319        mNextButton.setEnabled(enabled);
320        Utility.setCompoundDrawablesAlpha(mNextButton, enabled ? 255 : 128);
321        return enabled;
322    }
323
324    private void doOptions(PolicySet policySet) {
325        boolean easFlowMode = getIntent().getBooleanExtra(EXTRA_EAS_FLOW, false);
326        AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault, easFlowMode, policySet);
327        finish();
328    }
329
330    /**
331     * There are three cases handled here, so we split out into separate sections.
332     * 1.  Validate existing account (edit)
333     * 2.  Validate new account
334     * 3.  Autodiscover for new account
335     *
336     * For each case, there are two or more paths for success or failure.
337     */
338    @Override
339    public void onActivityResult(int requestCode, int resultCode, Intent data) {
340        if (requestCode == AccountSetupCheckSettings.REQUEST_CODE_VALIDATE) {
341            if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
342                doActivityResultValidateExistingAccount(resultCode, data);
343            } else {
344                doActivityResultValidateNewAccount(resultCode, data);
345            }
346        } else if (requestCode == AccountSetupCheckSettings.REQUEST_CODE_AUTO_DISCOVER) {
347            doActivityResultAutoDiscoverNewAccount(resultCode, data);
348        }
349    }
350
351    /**
352     * Process activity result when validating existing account
353     */
354    private void doActivityResultValidateExistingAccount(int resultCode, Intent data) {
355        if (resultCode == RESULT_OK) {
356            if (mAccount.isSaved()) {
357                // Account.update will NOT save the HostAuth's
358                mAccount.update(this, mAccount.toContentValues());
359                mAccount.mHostAuthRecv.update(this,
360                        mAccount.mHostAuthRecv.toContentValues());
361                mAccount.mHostAuthSend.update(this,
362                        mAccount.mHostAuthSend.toContentValues());
363                if (mAccount.mHostAuthRecv.mProtocol.equals("eas")) {
364                    // For EAS, notify SyncManager that the password has changed
365                    try {
366                        ExchangeUtils.getExchangeEmailService(this, null)
367                        .hostChanged(mAccount.mId);
368                    } catch (RemoteException e) {
369                        // Nothing to be done if this fails
370                    }
371                }
372            } else {
373                // Account.save will save the HostAuth's
374                mAccount.save(this);
375            }
376            // Update the backup (side copy) of the accounts
377            AccountBackupRestore.backupAccounts(this);
378            finish();
379        }
380        // else (resultCode not OK) - just return into this activity for further editing
381    }
382
383    /**
384     * Process activity result when validating new account
385     */
386    private void doActivityResultValidateNewAccount(int resultCode, Intent data) {
387        if (resultCode == RESULT_OK) {
388            // Go directly to next screen
389            PolicySet ps = null;
390            if ((data != null) && data.hasExtra(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET)) {
391                ps = (PolicySet)data.getParcelableExtra(
392                        EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET);
393            }
394            doOptions(ps);
395        } else if (resultCode == AccountSetupCheckSettings.RESULT_SECURITY_REQUIRED_USER_CANCEL) {
396            finish();
397        }
398        // else (resultCode not OK) - just return into this activity for further editing
399    }
400
401    /**
402     * Process activity result when validating new account
403     */
404    private void doActivityResultAutoDiscoverNewAccount(int resultCode, Intent data) {
405        // If authentication failed, exit immediately (to re-enter credentials)
406        if (resultCode == AccountSetupCheckSettings.RESULT_AUTO_DISCOVER_AUTH_FAILED) {
407            finish();
408            return;
409        }
410
411        // If data was returned, populate the account & populate the UI fields and validate it
412        if (data != null) {
413            Parcelable p = data.getParcelableExtra("HostAuth");
414            if (p != null) {
415                HostAuth hostAuth = (HostAuth)p;
416                mAccount.mHostAuthSend = hostAuth;
417                mAccount.mHostAuthRecv = hostAuth;
418                loadFields(mAccount);
419                if (validateFields()) {
420                    // "click" next to launch server verification
421                    onNext();
422                }
423            }
424        }
425        // Otherwise, proceed into this activity for manual setup
426    }
427
428    /**
429     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
430     * a problem with the user input.
431     * @return a URI built from the account setup fields
432     */
433    private URI getUri() throws URISyntaxException {
434        boolean sslRequired = mSslSecurityView.isChecked();
435        boolean trustCertificates = mTrustCertificatesView.isChecked();
436        String scheme = (sslRequired)
437                        ? (trustCertificates ? "eas+ssl+trustallcerts" : "eas+ssl+")
438                        : "eas";
439        String userName = mUsernameView.getText().toString().trim();
440        // Remove a leading backslash, if there is one, since we now automatically put one at
441        // the start of the username field
442        if (userName.startsWith("\\")) {
443            userName = userName.substring(1);
444        }
445        mCacheLoginCredential = userName;
446        String userInfo = userName + ":" + mPasswordView.getText().toString().trim();
447        String host = mServerView.getText().toString().trim();
448        String path = null;
449
450        URI uri = new URI(
451                scheme,
452                userInfo,
453                host,
454                0,
455                path,
456                null,
457                null);
458
459        return uri;
460    }
461
462    /**
463     * Note, in EAS, store & sender are the same, so we always populate them together
464     */
465    private void onNext() {
466        try {
467            URI uri = getUri();
468            mAccount.setStoreUri(this, uri.toString());
469            mAccount.setSenderUri(this, uri.toString());
470
471            // Stop here if the login credentials duplicate an existing account
472            // (unless they duplicate the existing account, as they of course will)
473            Account account = Utility.findExistingAccount(this, mAccount.mId,
474                    uri.getHost(), mCacheLoginCredential);
475            if (account != null) {
476                mDuplicateAccountName = account.mDisplayName;
477                this.showDialog(DIALOG_DUPLICATE_ACCOUNT);
478                return;
479            }
480        } catch (URISyntaxException use) {
481            /*
482             * It's unrecoverable if we cannot create a URI from components that
483             * we validated to be safe.
484             */
485            throw new Error(use);
486        }
487
488        AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, false);
489    }
490
491    public void onClick(View v) {
492        switch (v.getId()) {
493            case R.id.next:
494                onNext();
495                break;
496        }
497    }
498
499    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
500        if (buttonView.getId() == R.id.account_ssl) {
501            mTrustCertificatesView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
502        }
503    }
504}
505