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