AccountCheckSettingsFragment.java revision 76472ae40cd55d17edb0420e8fc2a7bae60c50de
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 android.app.Activity;
20import android.app.Fragment;
21import android.app.FragmentManager;
22import android.content.Context;
23import android.os.AsyncTask;
24import android.os.Bundle;
25
26import com.android.email.R;
27import com.android.email.mail.Sender;
28import com.android.email.mail.Store;
29import com.android.email.service.EmailServiceUtils;
30import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
31import com.android.emailcommon.Logging;
32import com.android.emailcommon.mail.MessagingException;
33import com.android.emailcommon.provider.Account;
34import com.android.emailcommon.provider.HostAuth;
35import com.android.emailcommon.provider.Policy;
36import com.android.emailcommon.service.EmailServiceProxy;
37import com.android.emailcommon.utility.Utility;
38import com.android.mail.utils.LogUtils;
39
40/**
41 * Check incoming or outgoing settings, or perform autodiscovery.
42 *
43 * There are three components that work together.  1. This fragment is retained and non-displayed,
44 * and controls the overall process.  2. An AsyncTask that works with the stores/services to
45 * check the accounts settings.  3. A stateless progress dialog (which will be recreated on
46 * orientation changes).
47 *
48 * There are also two lightweight error dialogs which are used for notification of terminal
49 * conditions.
50 */
51public class AccountCheckSettingsFragment extends Fragment {
52
53    public final static String TAG = "AccountCheckStgFrag";
54
55    // State
56    private final static int STATE_START = 0;
57    private final static int STATE_CHECK_AUTODISCOVER = 1;
58    private final static int STATE_CHECK_INCOMING = 2;
59    private final static int STATE_CHECK_OUTGOING = 3;
60    private final static int STATE_CHECK_OK = 4;                    // terminal
61    private final static int STATE_CHECK_SHOW_SECURITY = 5;         // terminal
62    private final static int STATE_CHECK_ERROR = 6;                 // terminal
63    private final static int STATE_AUTODISCOVER_AUTH_DIALOG = 7;    // terminal
64    private final static int STATE_AUTODISCOVER_RESULT = 8;         // terminal
65    private int mState = STATE_START;
66
67    // Args
68    private final static String ARGS_MODE = "mode";
69
70    private int mMode;
71
72    // Support for UI
73    private boolean mAttached;
74    private boolean mPaused = false;
75    private MessagingException mProgressException;
76
77    // Support for AsyncTask and account checking
78    AccountCheckTask mAccountCheckTask;
79
80    // Result codes returned by onCheckSettingsAutoDiscoverComplete.
81    /** AutoDiscover completed successfully with server setup data */
82    public final static int AUTODISCOVER_OK = 0;
83    /** AutoDiscover completed with no data (no server or AD not supported) */
84    public final static int AUTODISCOVER_NO_DATA = 1;
85    /** AutoDiscover reported authentication error */
86    public final static int AUTODISCOVER_AUTHENTICATION = 2;
87
88    /**
89     * Callback interface for any target or activity doing account check settings
90     */
91    public interface Callback {
92        /**
93         * Called when CheckSettings completed
94         */
95        void onCheckSettingsComplete();
96
97        /**
98         * Called when we determine that a security policy will need to be installed
99         * @param hostName Passed back from the MessagingException
100         */
101        void onCheckSettingsSecurityRequired(String hostName);
102
103        /**
104         * Called when we receive an error while validating the account
105         * @param reason from
106         *      {@link CheckSettingsErrorDialogFragment#getReasonFromException(MessagingException)}
107         * @param message from
108         *      {@link CheckSettingsErrorDialogFragment#getErrorString(Context, MessagingException)}
109         */
110        void onCheckSettingsError(int reason, String message);
111
112        /**
113         * Called when autodiscovery completes.
114         * @param result autodiscovery result code - success is AUTODISCOVER_OK
115         */
116        void onCheckSettingsAutoDiscoverComplete(int result);
117    }
118
119    // Public no-args constructor needed for fragment re-instantiation
120    public AccountCheckSettingsFragment() {}
121
122    /**
123     * Create a retained, invisible fragment that checks accounts
124     *
125     * @param mode incoming or outgoing
126     */
127    public static AccountCheckSettingsFragment newInstance(int mode) {
128        final AccountCheckSettingsFragment f = new AccountCheckSettingsFragment();
129        final Bundle b = new Bundle(1);
130        b.putInt(ARGS_MODE, mode);
131        f.setArguments(b);
132        return f;
133    }
134
135    /**
136     * Fragment initialization.  Because we never implement onCreateView, and call
137     * setRetainInstance here, this creates an invisible, persistent, "worker" fragment.
138     */
139    @Override
140    public void onCreate(Bundle savedInstanceState) {
141        super.onCreate(savedInstanceState);
142        setRetainInstance(true);
143        mMode = getArguments().getInt(ARGS_MODE);
144    }
145
146    /**
147     * This is called when the Fragment's Activity is ready to go, after
148     * its content view has been installed; it is called both after
149     * the initial fragment creation and after the fragment is re-attached
150     * to a new activity.
151     */
152    @Override
153    public void onActivityCreated(Bundle savedInstanceState) {
154        super.onActivityCreated(savedInstanceState);
155        mAttached = true;
156
157        // If this is the first time, start the AsyncTask
158        if (mAccountCheckTask == null) {
159            final SetupDataFragment.SetupDataContainer container =
160                    (SetupDataFragment.SetupDataContainer) getActivity();
161            // TODO: don't pass in the whole SetupDataFragment
162            mAccountCheckTask = (AccountCheckTask)
163                    new AccountCheckTask(getActivity().getApplicationContext(), this, mMode,
164                            container.getSetupData())
165                    .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
166        }
167    }
168
169    /**
170     * When resuming, restart the progress/error UI if necessary by re-reporting previous values
171     */
172    @Override
173    public void onResume() {
174        super.onResume();
175        mPaused = false;
176
177        if (mState != STATE_START) {
178            reportProgress(mState, mProgressException);
179        }
180    }
181
182    @Override
183    public void onPause() {
184        super.onPause();
185        mPaused = true;
186    }
187
188    /**
189     * This is called when the fragment is going away.  It is NOT called
190     * when the fragment is being propagated between activity instances.
191     */
192    @Override
193    public void onDestroy() {
194        super.onDestroy();
195        if (mAccountCheckTask != null) {
196            Utility.cancelTaskInterrupt(mAccountCheckTask);
197            mAccountCheckTask = null;
198        }
199    }
200
201    /**
202     * This is called right before the fragment is detached from its current activity instance.
203     * All reporting and callbacks are halted until we reattach.
204     */
205    @Override
206    public void onDetach() {
207        super.onDetach();
208        mAttached = false;
209    }
210
211    /**
212     * The worker (AsyncTask) will call this (in the UI thread) to report progress.  If we are
213     * attached to an activity, update the progress immediately;  If not, simply hold the
214     * progress for later.
215     * @param newState The new progress state being reported
216     */
217    private void reportProgress(int newState, MessagingException ex) {
218        mState = newState;
219        mProgressException = ex;
220
221        // If we are attached, create, recover, and/or update the dialog
222        if (mAttached && !mPaused) {
223            final FragmentManager fm = getFragmentManager();
224
225            switch (newState) {
226                case STATE_CHECK_OK:
227                    // immediately terminate, clean up, and report back
228                    getCallbackTarget().onCheckSettingsComplete();
229                    break;
230                case STATE_CHECK_SHOW_SECURITY:
231                    // report that we need to accept a security policy
232                    String hostName = ex.getMessage();
233                    if (hostName != null) {
234                        hostName = hostName.trim();
235                    }
236                    getCallbackTarget().onCheckSettingsSecurityRequired(hostName);
237                    break;
238                case STATE_CHECK_ERROR:
239                case STATE_AUTODISCOVER_AUTH_DIALOG:
240                    // report that we had an error
241                    final int reason =
242                            CheckSettingsErrorDialogFragment.getReasonFromException(ex);
243                    final String errorMessage =
244                            CheckSettingsErrorDialogFragment.getErrorString(getActivity(), ex);
245                    getCallbackTarget().onCheckSettingsError(reason, errorMessage);
246                    break;
247                case STATE_AUTODISCOVER_RESULT:
248                    final HostAuth autoDiscoverResult = ((AutoDiscoverResults) ex).mHostAuth;
249                    // report autodiscover results back to target fragment or activity
250                    getCallbackTarget().onCheckSettingsAutoDiscoverComplete(
251                            (autoDiscoverResult != null) ? AUTODISCOVER_OK : AUTODISCOVER_NO_DATA);
252                    break;
253                default:
254                    // Display a normal progress message
255                    CheckSettingsProgressDialogFragment checkingDialog =
256                            (CheckSettingsProgressDialogFragment)
257                                    fm.findFragmentByTag(CheckSettingsProgressDialogFragment.TAG);
258
259                    if (checkingDialog != null) {
260                        checkingDialog.updateProgress(mState);
261                    }
262                    break;
263            }
264        }
265    }
266
267    /**
268     * Find the callback target, either a target fragment or the activity
269     */
270    private Callback getCallbackTarget() {
271        final Fragment target = getTargetFragment();
272        if (target instanceof Callback) {
273            return (Callback) target;
274        }
275        Activity activity = getActivity();
276        if (activity instanceof Callback) {
277            return (Callback) activity;
278        }
279        throw new IllegalStateException();
280    }
281
282    /**
283     * This exception class is used to report autodiscover results via the reporting mechanism.
284     */
285    public static class AutoDiscoverResults extends MessagingException {
286        public final HostAuth mHostAuth;
287
288        /**
289         * @param authenticationError true if auth failure, false for result (or no response)
290         * @param hostAuth null for "no autodiscover", non-null for server info to return
291         */
292        public AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth) {
293            super(null);
294            if (authenticationError) {
295                mExceptionType = AUTODISCOVER_AUTHENTICATION_FAILED;
296            } else {
297                mExceptionType = AUTODISCOVER_AUTHENTICATION_RESULT;
298            }
299            mHostAuth = hostAuth;
300        }
301    }
302
303    /**
304     * This AsyncTask does the actual account checking
305     *
306     * TODO: It would be better to remove the UI complete from here (the exception->string
307     * conversions).
308     */
309    private static class AccountCheckTask extends AsyncTask<Void, Integer, MessagingException> {
310        final Context mContext;
311        final AccountCheckSettingsFragment mCallback;
312        final int mMode;
313        final SetupDataFragment mSetupData;
314        final Account mAccount;
315        final String mStoreHost;
316        final String mCheckEmail;
317        final String mCheckPassword;
318
319        /**
320         * Create task and parameterize it
321         * @param context application context object
322         * @param mode bits request operations
323         * @param setupData {@link SetupDataFragment} holding values to be checked
324         */
325        public AccountCheckTask(Context context, AccountCheckSettingsFragment callback, int mode,
326                SetupDataFragment setupData) {
327            mContext = context;
328            mCallback = callback;
329            mMode = mode;
330            mSetupData = setupData;
331            mAccount = setupData.getAccount();
332            mStoreHost = mAccount.mHostAuthRecv.mAddress;
333            mCheckEmail = mAccount.mEmailAddress;
334            mCheckPassword = mAccount.mHostAuthRecv.mPassword;
335        }
336
337        @Override
338        protected MessagingException doInBackground(Void... params) {
339            try {
340                if ((mMode & SetupDataFragment.CHECK_AUTODISCOVER) != 0) {
341                    if (isCancelled()) return null;
342                    LogUtils.d(Logging.LOG_TAG, "Begin auto-discover for %s", mCheckEmail);
343                    publishProgress(STATE_CHECK_AUTODISCOVER);
344                    final Store store = Store.getInstance(mAccount, mContext);
345                    final Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword);
346                    // Result will be one of:
347                    //  null: remote exception - proceed to manual setup
348                    //  MessagingException.AUTHENTICATION_FAILED: username/password rejected
349                    //  Other error: proceed to manual setup
350                    //  No error: return autodiscover results
351                    if (result == null) {
352                        return new AutoDiscoverResults(false, null);
353                    }
354                    int errorCode =
355                            result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
356                    if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) {
357                        return new AutoDiscoverResults(true, null);
358                    } else if (errorCode != MessagingException.NO_ERROR) {
359                        return new AutoDiscoverResults(false, null);
360                    } else {
361                        HostAuth serverInfo =
362                            result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH);
363                        return new AutoDiscoverResults(false, serverInfo);
364                    }
365                }
366
367                // Check Incoming Settings
368                if ((mMode & SetupDataFragment.CHECK_INCOMING) != 0) {
369                    if (isCancelled()) return null;
370                    LogUtils.d(Logging.LOG_TAG, "Begin check of incoming email settings");
371                    publishProgress(STATE_CHECK_INCOMING);
372                    final Store store = Store.getInstance(mAccount, mContext);
373                    final Bundle bundle = store.checkSettings();
374                    if (bundle == null) {
375                        return new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION);
376                    }
377                    mAccount.mProtocolVersion = bundle.getString(
378                            EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION);
379                    int resultCode = bundle.getInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE);
380                    final String redirectAddress = bundle.getString(
381                            EmailServiceProxy.VALIDATE_BUNDLE_REDIRECT_ADDRESS, null);
382                    if (redirectAddress != null) {
383                        mAccount.mHostAuthRecv.mAddress = redirectAddress;
384                    }
385                    // Only show "policies required" if this is a new account setup
386                    if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED &&
387                            mAccount.isSaved()) {
388                        resultCode = MessagingException.NO_ERROR;
389                    }
390                    if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) {
391                        mSetupData.setPolicy((Policy)bundle.getParcelable(
392                                EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET));
393                        return new MessagingException(resultCode, mStoreHost);
394                    } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) {
395                        final Policy policy = bundle.getParcelable(
396                                EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET);
397                        final String unsupported = policy.mProtocolPoliciesUnsupported;
398                        final String[] data =
399                                unsupported.split("" + Policy.POLICY_STRING_DELIMITER);
400                        return new MessagingException(resultCode, mStoreHost, data);
401                    } else if (resultCode != MessagingException.NO_ERROR) {
402                        final String errorMessage;
403                        errorMessage = bundle.getString(
404                                EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
405                        return new MessagingException(resultCode, errorMessage);
406                    }
407                }
408
409                final String protocol = mAccount.mHostAuthRecv.mProtocol;
410                final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext, protocol);
411
412                // Check Outgoing Settings
413                if (info.usesSmtp && (mMode & SetupDataFragment.CHECK_OUTGOING) != 0) {
414                    if (isCancelled()) return null;
415                    LogUtils.d(Logging.LOG_TAG, "Begin check of outgoing email settings");
416                    publishProgress(STATE_CHECK_OUTGOING);
417                    final Sender sender = Sender.getInstance(mContext, mAccount);
418                    sender.close();
419                    sender.open();
420                    sender.close();
421                }
422
423                // If we reached the end, we completed the check(s) successfully
424                return null;
425            } catch (final MessagingException me) {
426                // Some of the legacy account checkers return errors by throwing MessagingException,
427                // which we catch and return here.
428                return me;
429            }
430        }
431
432        /**
433         * Progress reports (runs in UI thread).  This should be used for real progress only
434         * (not for errors).
435         */
436        @Override
437        protected void onProgressUpdate(Integer... progress) {
438            if (isCancelled()) return;
439            mCallback.reportProgress(progress[0], null);
440        }
441
442        /**
443         * Result handler (runs in UI thread).
444         *
445         * AutoDiscover authentication errors are handled a bit differently than the
446         * other errors;  If encountered, we display the error dialog, but we return with
447         * a different callback used only for AutoDiscover.
448         *
449         * @param result null for a successful check;  exception for various errors
450         */
451        @Override
452        protected void onPostExecute(MessagingException result) {
453            if (isCancelled()) return;
454            if (result == null) {
455                mCallback.reportProgress(STATE_CHECK_OK, null);
456            } else {
457                int progressState = STATE_CHECK_ERROR;
458                final int exceptionType = result.getExceptionType();
459
460                switch (exceptionType) {
461                    // NOTE: AutoDiscover reports have their own reporting state, handle differently
462                    // from the other exception types
463                    case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
464                        progressState = STATE_AUTODISCOVER_AUTH_DIALOG;
465                        break;
466                    case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT:
467                        progressState = STATE_AUTODISCOVER_RESULT;
468                        break;
469                    // NOTE: Security policies required has its own report state, handle it a bit
470                    // differently from the other exception types.
471                    case MessagingException.SECURITY_POLICIES_REQUIRED:
472                        progressState = STATE_CHECK_SHOW_SECURITY;
473                        break;
474                }
475                mCallback.reportProgress(progressState, result);
476            }
477        }
478    }
479
480    /**
481     * Convert progress to message
482     */
483    protected static String getProgressString(Context context, int progress) {
484        int stringId = 0;
485        switch (progress) {
486            case STATE_CHECK_AUTODISCOVER:
487                stringId = R.string.account_setup_check_settings_retr_info_msg;
488                break;
489            case STATE_START:
490            case STATE_CHECK_INCOMING:
491                stringId = R.string.account_setup_check_settings_check_incoming_msg;
492                break;
493            case STATE_CHECK_OUTGOING:
494                stringId = R.string.account_setup_check_settings_check_outgoing_msg;
495                break;
496        }
497        if (stringId != 0) {
498            return context.getString(stringId);
499        } else {
500            return null;
501        }
502    }
503
504    /**
505     * Convert mode to initial progress
506     */
507    protected static int getProgressForMode(int checkMode) {
508        switch (checkMode) {
509            case SetupDataFragment.CHECK_INCOMING:
510                return STATE_CHECK_INCOMING;
511            case SetupDataFragment.CHECK_OUTGOING:
512                return STATE_CHECK_OUTGOING;
513            case SetupDataFragment.CHECK_AUTODISCOVER:
514                return STATE_CHECK_AUTODISCOVER;
515        }
516        return STATE_START;
517    }
518}
519