AccountSetupExchangeFragment.java revision 7b5c5cf2a993d68bb8fe1a65bde3184e8d59c10f
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.SyncManager;
27
28import android.app.Activity;
29import android.app.Fragment;
30import android.content.Context;
31import android.os.Bundle;
32import android.os.RemoteException;
33import android.text.Editable;
34import android.text.TextWatcher;
35import android.util.Log;
36import android.view.LayoutInflater;
37import android.view.View;
38import android.view.ViewGroup;
39import android.widget.CheckBox;
40import android.widget.CompoundButton;
41import android.widget.CompoundButton.OnCheckedChangeListener;
42import android.widget.EditText;
43import android.widget.TextView;
44
45import java.io.IOException;
46import java.net.URI;
47import java.net.URISyntaxException;
48
49/**
50 * Provides generic setup for Exchange accounts.
51 *
52 * TODO: The manifest for this activity has it ignore config changes, because
53 * we don't want to restart on every orientation - this would launch autodiscover again.
54 * Do not attempt to define orientation-specific resources, they won't be loaded.
55 * What we really need here is a more "sticky" way to prevent that problem.
56 */
57public class AccountSetupExchangeFragment extends Fragment implements OnCheckedChangeListener {
58
59    private final static String STATE_KEY_CREDENTIAL =
60        "AccountSetupExchangeFragment.loginCredential";
61
62    private EditText mUsernameView;
63    private EditText mPasswordView;
64    private EditText mServerView;
65    private CheckBox mSslSecurityView;
66    private CheckBox mTrustCertificatesView;
67
68    // Support for lifecycle
69    private Context mContext;
70    private Callback mCallback = EmptyCallback.INSTANCE;
71    private boolean mStarted;
72    private boolean mLoaded;
73    private String mCacheLoginCredential;
74
75    /**
76     * Callback interface that owning activities must implement
77     */
78    public interface Callback {
79        public void onEnableProceedButtons(boolean enable);
80        public void onProceedNext();
81    }
82
83    private static class EmptyCallback implements Callback {
84        public static final Callback INSTANCE = new EmptyCallback();
85        @Override public void onProceedNext() { }
86        @Override public void onEnableProceedButtons(boolean enable) { }
87    }
88
89    /**
90     * Called to do initial creation of a fragment.  This is called after
91     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
92     */
93    @Override
94    public void onCreate(Bundle savedInstanceState) {
95        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
96            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onCreate");
97        }
98        super.onCreate(savedInstanceState);
99
100        if (savedInstanceState != null) {
101            mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL);
102        }
103    }
104
105    @Override
106    public View onCreateView(LayoutInflater inflater, ViewGroup container,
107            Bundle savedInstanceState) {
108        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
109            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onCreateView");
110        }
111        View view = inflater.inflate(R.layout.account_setup_exchange_fragment, container, false);
112        Context context = getActivity();
113
114        mUsernameView = (EditText) view.findViewById(R.id.account_username);
115        mPasswordView = (EditText) view.findViewById(R.id.account_password);
116        mServerView = (EditText) view.findViewById(R.id.account_server);
117        mSslSecurityView = (CheckBox) view.findViewById(R.id.account_ssl);
118        mSslSecurityView.setOnCheckedChangeListener(this);
119        mTrustCertificatesView = (CheckBox) view.findViewById(R.id.account_trust_certificates);
120
121        // Calls validateFields() which enables or disables the Next button
122        // based on the fields' validity.
123        TextWatcher validationTextWatcher = new TextWatcher() {
124            public void afterTextChanged(Editable s) {
125                validateFields();
126            }
127
128            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
129            public void onTextChanged(CharSequence s, int start, int before, int count) { }
130        };
131        mUsernameView.addTextChangedListener(validationTextWatcher);
132        mPasswordView.addTextChangedListener(validationTextWatcher);
133        mServerView.addTextChangedListener(validationTextWatcher);
134
135        //EXCHANGE-REMOVE-SECTION-START
136        // Show device ID
137        try {
138            String deviceId = SyncManager.getDeviceId(context);
139            ((TextView) view.findViewById(R.id.device_id)).setText(deviceId);
140        } catch (IOException ignore) {
141            // There's nothing we can do here...
142        }
143        //EXCHANGE-REMOVE-SECTION-END
144
145        return view;
146    }
147
148    @Override
149    public void onActivityCreated(Bundle savedInstanceState) {
150        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
151            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onActivityCreated");
152        }
153        super.onActivityCreated(savedInstanceState);
154    }
155
156    /**
157     * Called when the Fragment is visible to the user.
158     */
159    @Override
160    public void onStart() {
161        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
162            Log.d(Email.LOG_TAG, "AccountSetupExchangeFragment onStart");
163        }
164        super.onStart();
165        mStarted = true;
166        if (!mLoaded) {
167            loadSettings(SetupData.getAccount());
168        }
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    }
223
224    /**
225     * Activity provides callbacks here.  This also triggers loading and setting up the UX
226     */
227    public void setCallback(Callback callback) {
228        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
229        mContext = getActivity();
230        if (mStarted && !mLoaded) {
231            loadSettings(SetupData.getAccount());
232        }
233    }
234
235    /**
236     * Load the current settings into the UI
237     *
238     * @return true if the loaded values pass validation
239     */
240    /* package */ boolean loadSettings(Account account) {
241        HostAuth hostAuth = account.mHostAuthRecv;
242
243        String userName = hostAuth.mLogin;
244        if (userName != null) {
245            // Add a backslash to the start of the username, but only if the username has no
246            // backslash in it.
247            if (userName.indexOf('\\') < 0) {
248                userName = "\\" + userName;
249            }
250            mUsernameView.setText(userName);
251        }
252
253        if (hostAuth.mPassword != null) {
254            mPasswordView.setText(hostAuth.mPassword);
255        }
256
257        String protocol = hostAuth.mProtocol;
258        if (protocol == null || !protocol.startsWith("eas")) {
259            throw new Error("Unknown account type: " + account.getStoreUri(mContext));
260        }
261
262        if (hostAuth.mAddress != null) {
263            mServerView.setText(hostAuth.mAddress);
264        }
265
266        boolean ssl = 0 != (hostAuth.mFlags & HostAuth.FLAG_SSL);
267        boolean trustCertificates = 0 != (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES);
268        mSslSecurityView.setChecked(ssl);
269        mTrustCertificatesView.setChecked(trustCertificates);
270        mTrustCertificatesView.setVisibility(ssl ? View.VISIBLE : View.GONE);
271
272        return validateFields();
273    }
274
275    private boolean usernameFieldValid(EditText usernameView) {
276        return Utility.isTextViewNotEmpty(usernameView) &&
277            !usernameView.getText().toString().equals("\\");
278    }
279
280    /**
281     * Check the values in the fields and decide if it makes sense to enable the "next" button
282     * @return true if all fields are valid, false if any fields are incomplete
283     */
284    private boolean validateFields() {
285        boolean enabled = usernameFieldValid(mUsernameView)
286                && Utility.isTextViewNotEmpty(mPasswordView)
287                && Utility.isTextViewNotEmpty(mServerView);
288        if (enabled) {
289            try {
290                URI uri = getUri();
291            } catch (URISyntaxException use) {
292                enabled = false;
293            }
294        }
295        mCallback.onEnableProceedButtons(enabled);
296        return enabled;
297    }
298
299    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
300        if (buttonView.getId() == R.id.account_ssl) {
301            mTrustCertificatesView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
302        }
303    }
304    /**
305     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
306     *
307     * TODO: Was the !isSaved() logic ever actually used?
308     */
309    public void saveSettingsAfterEdit() {
310        Account account = SetupData.getAccount();
311        if (account.isSaved()) {
312            // Account.update will NOT save the HostAuth's
313            account.update(mContext, account.toContentValues());
314            account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues());
315            account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues());
316            if (account.mHostAuthRecv.mProtocol.equals("eas")) {
317                // For EAS, notify SyncManager that the password has changed
318                try {
319                    ExchangeUtils.getExchangeEmailService(mContext, null).hostChanged(account.mId);
320                } catch (RemoteException e) {
321                    // Nothing to be done if this fails
322                }
323            }
324        } else {
325            // Account.save will save the HostAuth's
326            account.save(mContext);
327        }
328        // Update the backup (side copy) of the accounts
329        AccountBackupRestore.backupAccounts(mContext);
330    }
331
332    /**
333     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
334     */
335    public boolean setHostAuthFromAutodiscover(HostAuth newHostAuth) {
336        Account account = SetupData.getAccount();
337        account.mHostAuthSend = newHostAuth;
338        account.mHostAuthRecv = newHostAuth;
339        return loadSettings(account);
340    }
341
342    /**
343     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
344     * a problem with the user input.
345     * @return a URI built from the account setup fields
346     */
347    private URI getUri() throws URISyntaxException {
348        boolean sslRequired = mSslSecurityView.isChecked();
349        boolean trustCertificates = mTrustCertificatesView.isChecked();
350        String scheme = (sslRequired)
351                        ? (trustCertificates ? "eas+ssl+trustallcerts" : "eas+ssl+")
352                        : "eas";
353        String userName = mUsernameView.getText().toString().trim();
354        // Remove a leading backslash, if there is one, since we now automatically put one at
355        // the start of the username field
356        if (userName.startsWith("\\")) {
357            userName = userName.substring(1);
358        }
359        mCacheLoginCredential = userName;
360        String userInfo = userName + ":" + mPasswordView.getText().toString().trim();
361        String host = mServerView.getText().toString().trim();
362        String path = null;
363
364        URI uri = new URI(
365                scheme,
366                userInfo,
367                host,
368                0,
369                path,
370                null,
371                null);
372
373        return uri;
374    }
375
376    /**
377     * Entry point from Activity, when "next" button is clicked
378     */
379    public void onNext() {
380        try {
381            URI uri = getUri();
382            Account setupAccount = SetupData.getAccount();
383            setupAccount.setStoreUri(mContext, uri.toString());
384            setupAccount.setSenderUri(mContext, uri.toString());
385
386            // Stop here if the login credentials duplicate an existing account
387            // (unless they duplicate the existing account, as they of course will)
388            Account account = Utility.findExistingAccount(mContext, setupAccount.mId,
389                    uri.getHost(), mCacheLoginCredential);
390            if (account != null) {
391                DuplicateAccountDialogFragment dialogFragment =
392                    DuplicateAccountDialogFragment.newInstance(account.mDisplayName);
393                dialogFragment.show(getActivity(), DuplicateAccountDialogFragment.TAG);
394                return;
395            }
396        } catch (URISyntaxException use) {
397            /*
398             * It's unrecoverable if we cannot create a URI from components that
399             * we validated to be safe.
400             */
401            throw new Error(use);
402        }
403
404        mCallback.onProceedNext();
405    }
406}
407