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 mCheckEmail;
318        final String mCheckPassword;
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            mStoreHost = mAccount.mHostAuthRecv.mAddress;
334            mCheckEmail = mAccount.mEmailAddress;
335            mCheckPassword = mAccount.mHostAuthRecv.mPassword;
336        }
337
338        @Override
339        protected MessagingException doInBackground(Void... params) {
340            try {
341                if ((mMode & SetupDataFragment.CHECK_AUTODISCOVER) != 0) {
342                    if (isCancelled()) return null;
343                    LogUtils.d(Logging.LOG_TAG, "Begin auto-discover for %s", mCheckEmail);
344                    publishProgress(STATE_CHECK_AUTODISCOVER);
345                    final Store store = Store.getInstance(mAccount, mContext);
346                    final Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword);
347                    // Result will be one of:
348                    //  null: remote exception - proceed to manual setup
349                    //  MessagingException.AUTHENTICATION_FAILED: username/password rejected
350                    //  Other error: proceed to manual setup
351                    //  No error: return autodiscover results
352                    if (result == null) {
353                        return new AutoDiscoverResults(false, null);
354                    }
355                    int errorCode =
356                            result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
357                    if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) {
358                        return new AutoDiscoverResults(true, null);
359                    } else if (errorCode != MessagingException.NO_ERROR) {
360                        return new AutoDiscoverResults(false, null);
361                    } else {
362                        final HostAuthCompat hostAuthCompat =
363                            result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH);
364                        HostAuth serverInfo = null;
365                        if (hostAuthCompat != null) {
366                            serverInfo = hostAuthCompat.toHostAuth();
367                        }
368                        return new AutoDiscoverResults(false, serverInfo);
369                    }
370                }
371
372                // Check Incoming Settings
373                if ((mMode & SetupDataFragment.CHECK_INCOMING) != 0) {
374                    if (isCancelled()) return null;
375                    LogUtils.d(Logging.LOG_TAG, "Begin check of incoming email settings");
376                    publishProgress(STATE_CHECK_INCOMING);
377                    final Store store = Store.getInstance(mAccount, mContext);
378                    final Bundle bundle = store.checkSettings();
379                    if (bundle == null) {
380                        return new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION);
381                    }
382                    mAccount.mProtocolVersion = bundle.getString(
383                            EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION);
384                    int resultCode = bundle.getInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE);
385                    final String redirectAddress = bundle.getString(
386                            EmailServiceProxy.VALIDATE_BUNDLE_REDIRECT_ADDRESS, null);
387                    if (redirectAddress != null) {
388                        mAccount.mHostAuthRecv.mAddress = redirectAddress;
389                    }
390                    // Only show "policies required" if this is a new account setup
391                    if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED &&
392                            mAccount.isSaved()) {
393                        resultCode = MessagingException.NO_ERROR;
394                    }
395                    if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) {
396                        mSetupData.setPolicy((Policy)bundle.getParcelable(
397                                EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET));
398                        return new MessagingException(resultCode, mStoreHost);
399                    } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) {
400                        final Policy policy = bundle.getParcelable(
401                                EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET);
402                        final String unsupported = policy.mProtocolPoliciesUnsupported;
403                        final String[] data =
404                                unsupported.split("" + Policy.POLICY_STRING_DELIMITER);
405                        return new MessagingException(resultCode, mStoreHost, data);
406                    } else if (resultCode != MessagingException.NO_ERROR) {
407                        final String errorMessage;
408                        errorMessage = bundle.getString(
409                                EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
410                        return new MessagingException(resultCode, errorMessage);
411                    }
412                }
413
414                final String protocol = mAccount.mHostAuthRecv.mProtocol;
415                final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext, protocol);
416
417                // Check Outgoing Settings
418                if (info.usesSmtp && (mMode & SetupDataFragment.CHECK_OUTGOING) != 0) {
419                    if (isCancelled()) return null;
420                    LogUtils.d(Logging.LOG_TAG, "Begin check of outgoing email settings");
421                    publishProgress(STATE_CHECK_OUTGOING);
422                    final Sender sender = Sender.getInstance(mContext, mAccount);
423                    sender.close();
424                    sender.open();
425                    sender.close();
426                }
427
428                // If we reached the end, we completed the check(s) successfully
429                return null;
430            } catch (final MessagingException me) {
431                // Some of the legacy account checkers return errors by throwing MessagingException,
432                // which we catch and return here.
433                return me;
434            }
435        }
436
437        /**
438         * Progress reports (runs in UI thread).  This should be used for real progress only
439         * (not for errors).
440         */
441        @Override
442        protected void onProgressUpdate(Integer... progress) {
443            if (isCancelled()) return;
444            mCallback.reportProgress(progress[0], null);
445        }
446
447        /**
448         * Result handler (runs in UI thread).
449         *
450         * AutoDiscover authentication errors are handled a bit differently than the
451         * other errors;  If encountered, we display the error dialog, but we return with
452         * a different callback used only for AutoDiscover.
453         *
454         * @param result null for a successful check;  exception for various errors
455         */
456        @Override
457        protected void onPostExecute(MessagingException result) {
458            if (isCancelled()) return;
459            if (result == null) {
460                mCallback.reportProgress(STATE_CHECK_OK, null);
461            } else {
462                int progressState = STATE_CHECK_ERROR;
463                final int exceptionType = result.getExceptionType();
464
465                switch (exceptionType) {
466                    // NOTE: AutoDiscover reports have their own reporting state, handle differently
467                    // from the other exception types
468                    case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
469                        progressState = STATE_AUTODISCOVER_AUTH_DIALOG;
470                        break;
471                    case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT:
472                        progressState = STATE_AUTODISCOVER_RESULT;
473                        break;
474                    // NOTE: Security policies required has its own report state, handle it a bit
475                    // differently from the other exception types.
476                    case MessagingException.SECURITY_POLICIES_REQUIRED:
477                        progressState = STATE_CHECK_SHOW_SECURITY;
478                        break;
479                }
480                mCallback.reportProgress(progressState, result);
481            }
482        }
483    }
484
485    /**
486     * Convert progress to message
487     */
488    protected static String getProgressString(Context context, int progress) {
489        int stringId = 0;
490        switch (progress) {
491            case STATE_CHECK_AUTODISCOVER:
492                stringId = R.string.account_setup_check_settings_retr_info_msg;
493                break;
494            case STATE_START:
495            case STATE_CHECK_INCOMING:
496                stringId = R.string.account_setup_check_settings_check_incoming_msg;
497                break;
498            case STATE_CHECK_OUTGOING:
499                stringId = R.string.account_setup_check_settings_check_outgoing_msg;
500                break;
501        }
502        if (stringId != 0) {
503            return context.getString(stringId);
504        } else {
505            return null;
506        }
507    }
508
509    /**
510     * Convert mode to initial progress
511     */
512    protected static int getProgressForMode(int checkMode) {
513        switch (checkMode) {
514            case SetupDataFragment.CHECK_INCOMING:
515                return STATE_CHECK_INCOMING;
516            case SetupDataFragment.CHECK_OUTGOING:
517                return STATE_CHECK_OUTGOING;
518            case SetupDataFragment.CHECK_AUTODISCOVER:
519                return STATE_CHECK_AUTODISCOVER;
520        }
521        return STATE_START;
522    }
523}
524