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