AccountSetupExchangeFragment.java revision 112ed496f817ebeab6b1ee1d5117259ef80342b2
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity.setup;
18
19import com.android.email.AccountBackupRestore;
20import com.android.email.Email;
21import com.android.email.ExchangeUtils;
22import com.android.email.R;
23import com.android.email.Utility;
24import com.android.email.provider.EmailContent.Account;
25import com.android.email.provider.EmailContent.HostAuth;
26import com.android.exchange.ExchangeService;
27
28import android.app.Activity;
29import android.content.Context;
30import android.os.Bundle;
31import android.os.RemoteException;
32import android.text.Editable;
33import android.text.TextWatcher;
34import android.util.Log;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.ViewGroup;
38import android.widget.CheckBox;
39import android.widget.CompoundButton;
40import android.widget.CompoundButton.OnCheckedChangeListener;
41import android.widget.EditText;
42import android.widget.TextView;
43
44import java.io.IOException;
45import java.net.URI;
46import java.net.URISyntaxException;
47
48/**
49 * Provides generic setup for Exchange accounts.
50 *
51 * This fragment is used by AccountSetupExchange (for creating accounts) and by AccountSettingsXL
52 * (for editing existing accounts).
53 */
54public class AccountSetupExchangeFragment extends AccountServerBaseFragment
55        implements OnCheckedChangeListener {
56
57    private final static String STATE_KEY_CREDENTIAL = "AccountSetupExchangeFragment.credential";
58    private final static String STATE_KEY_LOADED = "AccountSetupExchangeFragment.loaded";
59
60    private EditText mUsernameView;
61    private EditText mPasswordView;
62    private EditText mServerView;
63    private CheckBox mSslSecurityView;
64    private CheckBox mTrustCertificatesView;
65    private View mTrustCertificatesDivider;
66
67    // Support for lifecycle
68    private boolean mStarted;
69    private boolean mLoaded;
70    private String mCacheLoginCredential;
71
72    /**
73     * Create the fragment with parameters - used mainly to force into settings mode (with buttons)
74     * @param settingsMode if true, alters appearance for use in settings (default is "setup")
75     */
76    public static AccountSetupExchangeFragment newInstance(boolean settingsMode) {
77        AccountSetupExchangeFragment f = new AccountSetupExchangeFragment();
78        f.setSetupArguments(settingsMode);
79        return f;
80    }
81
82    /**
83     * Called to do initial creation of a fragment.  This is called after
84     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
85     */
86    @Override
87    public void onCreate(Bundle savedInstanceState) {
88        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
89            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onCreate");
90        }
91        super.onCreate(savedInstanceState);
92
93        if (savedInstanceState != null) {
94            mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL);
95            mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
96        }
97    }
98
99    @Override
100    public View onCreateView(LayoutInflater inflater, ViewGroup container,
101            Bundle savedInstanceState) {
102        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
103            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onCreateView");
104        }
105        int layoutId = mSettingsMode
106                ? R.layout.account_settings_exchange_fragment
107                : R.layout.account_setup_exchange_fragment;
108
109        View view = inflater.inflate(layoutId, container, false);
110        Context context = getActivity();
111
112        mUsernameView = (EditText) view.findViewById(R.id.account_username);
113        mPasswordView = (EditText) view.findViewById(R.id.account_password);
114        mServerView = (EditText) view.findViewById(R.id.account_server);
115        mSslSecurityView = (CheckBox) view.findViewById(R.id.account_ssl);
116        mSslSecurityView.setOnCheckedChangeListener(this);
117        mTrustCertificatesView = (CheckBox) view.findViewById(R.id.account_trust_certificates);
118        mTrustCertificatesDivider = view.findViewById(R.id.account_trust_certificates_divider);
119
120        // Calls validateFields() which enables or disables the Next button
121        // based on the fields' validity.
122        TextWatcher validationTextWatcher = new TextWatcher() {
123            public void afterTextChanged(Editable s) {
124                validateFields();
125            }
126
127            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
128            public void onTextChanged(CharSequence s, int start, int before, int count) { }
129        };
130        mUsernameView.addTextChangedListener(validationTextWatcher);
131        mPasswordView.addTextChangedListener(validationTextWatcher);
132        mServerView.addTextChangedListener(validationTextWatcher);
133
134        //EXCHANGE-REMOVE-SECTION-START
135        // Show device ID
136        try {
137            String deviceId = ExchangeService.getDeviceId(context);
138            ((TextView) view.findViewById(R.id.device_id)).setText(deviceId);
139        } catch (IOException ignore) {
140            // There's nothing we can do here...
141        }
142        //EXCHANGE-REMOVE-SECTION-END
143
144        // Additional setup only used while in "settings" mode
145        onCreateViewSettingsMode(view);
146
147        return view;
148    }
149
150    @Override
151    public void onActivityCreated(Bundle savedInstanceState) {
152        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
153            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onActivityCreated");
154        }
155        super.onActivityCreated(savedInstanceState);
156    }
157
158    /**
159     * Called when the Fragment is visible to the user.
160     */
161    @Override
162    public void onStart() {
163        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
164            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onStart");
165        }
166        super.onStart();
167        mStarted = true;
168        loadSettings(SetupData.getAccount());
169    }
170
171    /**
172     * Called when the fragment is visible to the user and actively running.
173     */
174    @Override
175    public void onResume() {
176        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
177            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onResume");
178        }
179        super.onResume();
180        validateFields();
181    }
182
183    @Override
184    public void onPause() {
185        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
186            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onPause");
187        }
188        super.onPause();
189    }
190
191    /**
192     * Called when the Fragment is no longer started.
193     */
194    @Override
195    public void onStop() {
196        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
197            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onStop");
198        }
199        super.onStop();
200        mStarted = false;
201    }
202
203    /**
204     * Called when the fragment is no longer in use.
205     */
206    @Override
207    public void onDestroy() {
208        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
209            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onDestroy");
210        }
211        super.onDestroy();
212    }
213
214    @Override
215    public void onSaveInstanceState(Bundle outState) {
216        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
217            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onSaveInstanceState");
218        }
219        super.onSaveInstanceState(outState);
220
221        outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential);
222        outState.putBoolean(STATE_KEY_LOADED, mLoaded);
223    }
224
225    /**
226     * Activity provides callbacks here.  This also triggers loading and setting up the UX
227     */
228    @Override
229    public void setCallback(Callback callback) {
230        super.setCallback(callback);
231        if (mStarted) {
232            loadSettings(SetupData.getAccount());
233        }
234    }
235
236    /**
237     * Load the current settings into the UI
238     *
239     * @return true if the loaded values pass validation
240     */
241    /* package */ boolean loadSettings(Account account) {
242        if (mLoaded) return validateFields();
243
244        HostAuth hostAuth = account.mHostAuthRecv;
245
246        String userName = hostAuth.mLogin;
247        if (userName != null) {
248            // Add a backslash to the start of the username, but only if the username has no
249            // backslash in it.
250            if (userName.indexOf('\\') < 0) {
251                userName = "\\" + userName;
252            }
253            mUsernameView.setText(userName);
254        }
255
256        if (hostAuth.mPassword != null) {
257            mPasswordView.setText(hostAuth.mPassword);
258        }
259
260        String protocol = hostAuth.mProtocol;
261        if (protocol == null || !protocol.startsWith("eas")) {
262            throw new Error("Unknown account type: " + account.getStoreUri(mContext));
263        }
264
265        if (hostAuth.mAddress != null) {
266            mServerView.setText(hostAuth.mAddress);
267        }
268
269        boolean ssl = 0 != (hostAuth.mFlags & HostAuth.FLAG_SSL);
270        boolean trustCertificates = 0 != (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES);
271        mSslSecurityView.setChecked(ssl);
272        mTrustCertificatesView.setChecked(trustCertificates);
273        showTrustCertificates(ssl);
274
275        mLoaded = true;
276        return validateFields();
277    }
278
279    private boolean usernameFieldValid(EditText usernameView) {
280        return Utility.isTextViewNotEmpty(usernameView) &&
281            !usernameView.getText().toString().equals("\\");
282    }
283
284    /**
285     * Check the values in the fields and decide if it makes sense to enable the "next" button
286     * @return true if all fields are valid, false if any fields are incomplete
287     */
288    private boolean validateFields() {
289        if (!mLoaded) return false;
290        boolean enabled = usernameFieldValid(mUsernameView)
291                && Utility.isTextViewNotEmpty(mPasswordView)
292                && Utility.isTextViewNotEmpty(mServerView);
293        if (enabled) {
294            try {
295                URI uri = getUri();
296            } catch (URISyntaxException use) {
297                enabled = false;
298            }
299        }
300        enableNextButton(enabled);
301        return enabled;
302    }
303
304    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
305        if (buttonView.getId() == R.id.account_ssl) {
306            showTrustCertificates(isChecked);
307        }
308    }
309
310    public void showTrustCertificates(boolean visible) {
311        int mode = visible ? View.VISIBLE : View.GONE;
312        mTrustCertificatesView.setVisibility(mode);
313        // Divider is optional (only on XL layouts)
314        if (mTrustCertificatesDivider != null) {
315            mTrustCertificatesDivider.setVisibility(mode);
316        }
317    }
318
319    /**
320     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
321     *
322     * TODO: Was the !isSaved() logic ever actually used?
323     */
324    @Override
325    public void saveSettingsAfterEdit() {
326        Account account = SetupData.getAccount();
327        if (account.isSaved()) {
328            // Account.update will NOT save the HostAuth's
329            account.update(mContext, account.toContentValues());
330            account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues());
331            account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues());
332            if (account.mHostAuthRecv.mProtocol.equals("eas")) {
333                // For EAS, notify ExchangeService that the password has changed
334                try {
335                    ExchangeUtils.getExchangeService(mContext, null).hostChanged(account.mId);
336                } catch (RemoteException e) {
337                    // Nothing to be done if this fails
338                }
339            }
340        } else {
341            // Account.save will save the HostAuth's
342            account.save(mContext);
343        }
344        // Update the backup (side copy) of the accounts
345        AccountBackupRestore.backupAccounts(mContext);
346    }
347
348    /**
349     * This entry point is not used (unlike in AccountSetupIncomingFragment) because the data
350     * is already saved by onNext().
351     * TODO: Reconcile this, to make them more consistent.
352     */
353    @Override
354    public void saveSettingsAfterSetup() {
355    }
356
357    /**
358     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
359     */
360    public boolean setHostAuthFromAutodiscover(HostAuth newHostAuth) {
361        Account account = SetupData.getAccount();
362        account.mHostAuthSend = newHostAuth;
363        account.mHostAuthRecv = newHostAuth;
364        return loadSettings(account);
365    }
366
367    /**
368     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
369     * a problem with the user input.
370     * @return a URI built from the account setup fields
371     */
372    /* package */ URI getUri() throws URISyntaxException {
373        boolean sslRequired = mSslSecurityView.isChecked();
374        boolean trustCertificates = mTrustCertificatesView.isChecked();
375        String scheme = (sslRequired)
376                        ? (trustCertificates ? "eas+ssl+trustallcerts" : "eas+ssl+")
377                        : "eas";
378        String userName = mUsernameView.getText().toString().trim();
379        // Remove a leading backslash, if there is one, since we now automatically put one at
380        // the start of the username field
381        if (userName.startsWith("\\")) {
382            userName = userName.substring(1);
383        }
384        mCacheLoginCredential = userName;
385        String userInfo = userName + ":" + mPasswordView.getText();
386        String host = mServerView.getText().toString().trim();
387        String path = null;
388
389        URI uri = new URI(
390                scheme,
391                userInfo,
392                host,
393                0,
394                path,
395                null,
396                null);
397
398        return uri;
399    }
400
401    /**
402     * Implements AccountCheckSettingsFragment.Callbacks
403     */
404    @Override
405    public void onAutoDiscoverComplete(int result, HostAuth hostAuth) {
406        AccountSetupExchange activity = (AccountSetupExchange) getActivity();
407        activity.onAutoDiscoverComplete(result, hostAuth);
408    }
409
410    /**
411     * Entry point from Activity, when "next" button is clicked
412     */
413    @Override
414    public void onNext() {
415        try {
416            URI uri = getUri();
417            Account setupAccount = SetupData.getAccount();
418            setupAccount.setStoreUri(mContext, uri.toString());
419            setupAccount.setSenderUri(mContext, uri.toString());
420
421            // Stop here if the login credentials duplicate an existing account
422            // (unless they duplicate the existing account, as they of course will)
423            Account account = Utility.findExistingAccount(mContext, setupAccount.mId,
424                    uri.getHost(), mCacheLoginCredential);
425            if (account != null) {
426                DuplicateAccountDialogFragment dialogFragment =
427                    DuplicateAccountDialogFragment.newInstance(account.mDisplayName);
428                dialogFragment.show(getActivity(), DuplicateAccountDialogFragment.TAG);
429                return;
430            }
431        } catch (URISyntaxException use) {
432            /*
433             * It's unrecoverable if we cannot create a URI from components that
434             * we validated to be safe.
435             */
436            throw new Error(use);
437        }
438
439        mCallback.onProceedNext(SetupData.CHECK_INCOMING, this);
440    }
441}
442