AccountSetupExchangeFragment.java revision 2731aef45c6f2f9792ae698ebf7d65ca6338a02c
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 =
58        "AccountSetupExchangeFragment.loginCredential";
59
60    private EditText mUsernameView;
61    private EditText mPasswordView;
62    private EditText mServerView;
63    private CheckBox mSslSecurityView;
64    private CheckBox mTrustCertificatesView;
65
66    // Support for lifecycle
67    private boolean mStarted;
68    private boolean mLoaded;
69    private String mCacheLoginCredential;
70
71    /**
72     * Called to do initial creation of a fragment.  This is called after
73     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
74     */
75    @Override
76    public void onCreate(Bundle savedInstanceState) {
77        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
78            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onCreate");
79        }
80        super.onCreate(savedInstanceState);
81
82        if (savedInstanceState != null) {
83            mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL);
84        }
85    }
86
87    @Override
88    public View onCreateView(LayoutInflater inflater, ViewGroup container,
89            Bundle savedInstanceState) {
90        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
91            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onCreateView");
92        }
93        View view = inflater.inflate(R.layout.account_setup_exchange_fragment, container, false);
94        Context context = getActivity();
95
96        mUsernameView = (EditText) view.findViewById(R.id.account_username);
97        mPasswordView = (EditText) view.findViewById(R.id.account_password);
98        mServerView = (EditText) view.findViewById(R.id.account_server);
99        mSslSecurityView = (CheckBox) view.findViewById(R.id.account_ssl);
100        mSslSecurityView.setOnCheckedChangeListener(this);
101        mTrustCertificatesView = (CheckBox) view.findViewById(R.id.account_trust_certificates);
102
103        // Calls validateFields() which enables or disables the Next button
104        // based on the fields' validity.
105        TextWatcher validationTextWatcher = new TextWatcher() {
106            public void afterTextChanged(Editable s) {
107                validateFields();
108            }
109
110            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
111            public void onTextChanged(CharSequence s, int start, int before, int count) { }
112        };
113        mUsernameView.addTextChangedListener(validationTextWatcher);
114        mPasswordView.addTextChangedListener(validationTextWatcher);
115        mServerView.addTextChangedListener(validationTextWatcher);
116
117        //EXCHANGE-REMOVE-SECTION-START
118        // Show device ID
119        try {
120            String deviceId = ExchangeService.getDeviceId(context);
121            ((TextView) view.findViewById(R.id.device_id)).setText(deviceId);
122        } catch (IOException ignore) {
123            // There's nothing we can do here...
124        }
125        //EXCHANGE-REMOVE-SECTION-END
126
127        return view;
128    }
129
130    @Override
131    public void onActivityCreated(Bundle savedInstanceState) {
132        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
133            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onActivityCreated");
134        }
135        super.onActivityCreated(savedInstanceState);
136    }
137
138    /**
139     * Called when the Fragment is visible to the user.
140     */
141    @Override
142    public void onStart() {
143        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
144            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onStart");
145        }
146        super.onStart();
147        mStarted = true;
148        if (!mLoaded) {
149            loadSettings(SetupData.getAccount());
150        }
151    }
152
153    /**
154     * Called when the fragment is visible to the user and actively running.
155     */
156    @Override
157    public void onResume() {
158        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
159            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onResume");
160        }
161        super.onResume();
162        validateFields();
163    }
164
165    @Override
166    public void onPause() {
167        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
168            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onPause");
169        }
170        super.onPause();
171    }
172
173    /**
174     * Called when the Fragment is no longer started.
175     */
176    @Override
177    public void onStop() {
178        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
179            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onStop");
180        }
181        super.onStop();
182        mStarted = false;
183    }
184
185    /**
186     * Called when the fragment is no longer in use.
187     */
188    @Override
189    public void onDestroy() {
190        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
191            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onDestroy");
192        }
193        super.onDestroy();
194    }
195
196    @Override
197    public void onSaveInstanceState(Bundle outState) {
198        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
199            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onSaveInstanceState");
200        }
201        super.onSaveInstanceState(outState);
202
203        outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential);
204    }
205
206    /**
207     * Activity provides callbacks here.  This also triggers loading and setting up the UX
208     */
209    @Override
210    public void setCallback(Callback callback) {
211        super.setCallback(callback);
212        if (mStarted && !mLoaded) {
213            loadSettings(SetupData.getAccount());
214        }
215    }
216
217    /**
218     * Load the current settings into the UI
219     *
220     * @return true if the loaded values pass validation
221     */
222    /* package */ boolean loadSettings(Account account) {
223        HostAuth hostAuth = account.mHostAuthRecv;
224
225        String userName = hostAuth.mLogin;
226        if (userName != null) {
227            // Add a backslash to the start of the username, but only if the username has no
228            // backslash in it.
229            if (userName.indexOf('\\') < 0) {
230                userName = "\\" + userName;
231            }
232            mUsernameView.setText(userName);
233        }
234
235        if (hostAuth.mPassword != null) {
236            mPasswordView.setText(hostAuth.mPassword);
237        }
238
239        String protocol = hostAuth.mProtocol;
240        if (protocol == null || !protocol.startsWith("eas")) {
241            throw new Error("Unknown account type: " + account.getStoreUri(mContext));
242        }
243
244        if (hostAuth.mAddress != null) {
245            mServerView.setText(hostAuth.mAddress);
246        }
247
248        boolean ssl = 0 != (hostAuth.mFlags & HostAuth.FLAG_SSL);
249        boolean trustCertificates = 0 != (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES);
250        mSslSecurityView.setChecked(ssl);
251        mTrustCertificatesView.setChecked(trustCertificates);
252        mTrustCertificatesView.setVisibility(ssl ? View.VISIBLE : View.GONE);
253
254        return validateFields();
255    }
256
257    private boolean usernameFieldValid(EditText usernameView) {
258        return Utility.isTextViewNotEmpty(usernameView) &&
259            !usernameView.getText().toString().equals("\\");
260    }
261
262    /**
263     * Check the values in the fields and decide if it makes sense to enable the "next" button
264     * @return true if all fields are valid, false if any 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        enableNextButton(enabled);
278        return enabled;
279    }
280
281    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
282        if (buttonView.getId() == R.id.account_ssl) {
283            mTrustCertificatesView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
284        }
285    }
286    /**
287     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
288     *
289     * TODO: Was the !isSaved() logic ever actually used?
290     */
291    @Override
292    public void saveSettingsAfterEdit() {
293        Account account = SetupData.getAccount();
294        if (account.isSaved()) {
295            // Account.update will NOT save the HostAuth's
296            account.update(mContext, account.toContentValues());
297            account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues());
298            account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues());
299            if (account.mHostAuthRecv.mProtocol.equals("eas")) {
300                // For EAS, notify ExchangeService that the password has changed
301                try {
302                    ExchangeUtils.getExchangeService(mContext, null).hostChanged(account.mId);
303                } catch (RemoteException e) {
304                    // Nothing to be done if this fails
305                }
306            }
307        } else {
308            // Account.save will save the HostAuth's
309            account.save(mContext);
310        }
311        // Update the backup (side copy) of the accounts
312        AccountBackupRestore.backupAccounts(mContext);
313    }
314
315    /**
316     * This entry point is not used (unlike in AccountSetupIncomingFragment) because the data
317     * is already saved by onNext().
318     * TODO: Reconcile this, to make them more consistent.
319     */
320    @Override
321    public void saveSettingsAfterSetup() {
322    }
323
324    /**
325     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
326     */
327    public boolean setHostAuthFromAutodiscover(HostAuth newHostAuth) {
328        Account account = SetupData.getAccount();
329        account.mHostAuthSend = newHostAuth;
330        account.mHostAuthRecv = newHostAuth;
331        return loadSettings(account);
332    }
333
334    /**
335     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
336     * a problem with the user input.
337     * @return a URI built from the account setup fields
338     */
339    private URI getUri() throws URISyntaxException {
340        boolean sslRequired = mSslSecurityView.isChecked();
341        boolean trustCertificates = mTrustCertificatesView.isChecked();
342        String scheme = (sslRequired)
343                        ? (trustCertificates ? "eas+ssl+trustallcerts" : "eas+ssl+")
344                        : "eas";
345        String userName = mUsernameView.getText().toString().trim();
346        // Remove a leading backslash, if there is one, since we now automatically put one at
347        // the start of the username field
348        if (userName.startsWith("\\")) {
349            userName = userName.substring(1);
350        }
351        mCacheLoginCredential = userName;
352        String userInfo = userName + ":" + mPasswordView.getText().toString().trim();
353        String host = mServerView.getText().toString().trim();
354        String path = null;
355
356        URI uri = new URI(
357                scheme,
358                userInfo,
359                host,
360                0,
361                path,
362                null,
363                null);
364
365        return uri;
366    }
367
368    /**
369     * Implements AccountCheckSettingsFragment.Callbacks
370     */
371    @Override
372    public void onAutoDiscoverComplete(int result, HostAuth hostAuth) {
373        AccountSetupExchange activity = (AccountSetupExchange) getActivity();
374        activity.onAutoDiscoverComplete(result, hostAuth);
375    }
376
377    /**
378     * Entry point from Activity, when "next" button is clicked
379     */
380    @Override
381    public void onNext() {
382        try {
383            URI uri = getUri();
384            Account setupAccount = SetupData.getAccount();
385            setupAccount.setStoreUri(mContext, uri.toString());
386            setupAccount.setSenderUri(mContext, uri.toString());
387
388            // Stop here if the login credentials duplicate an existing account
389            // (unless they duplicate the existing account, as they of course will)
390            Account account = Utility.findExistingAccount(mContext, setupAccount.mId,
391                    uri.getHost(), mCacheLoginCredential);
392            if (account != null) {
393                DuplicateAccountDialogFragment dialogFragment =
394                    DuplicateAccountDialogFragment.newInstance(account.mDisplayName);
395                dialogFragment.show(getActivity(), DuplicateAccountDialogFragment.TAG);
396                return;
397            }
398        } catch (URISyntaxException use) {
399            /*
400             * It's unrecoverable if we cannot create a URI from components that
401             * we validated to be safe.
402             */
403            throw new Error(use);
404        }
405
406        mCallback.onProceedNext(SetupData.CHECK_INCOMING, this);
407    }
408}
409