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 
17 package com.android.email.activity.setup;
18 
19 import android.app.Activity;
20 import android.app.Fragment;
21 import android.app.FragmentManager;
22 import android.content.Context;
23 import android.os.AsyncTask;
24 import android.os.Bundle;
25 
26 import com.android.email.R;
27 import com.android.email.mail.Sender;
28 import com.android.email.mail.Store;
29 import com.android.email.service.EmailServiceUtils;
30 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
31 import com.android.emailcommon.Logging;
32 import com.android.emailcommon.mail.MessagingException;
33 import com.android.emailcommon.provider.Account;
34 import com.android.emailcommon.provider.HostAuth;
35 import com.android.emailcommon.provider.Policy;
36 import com.android.emailcommon.service.EmailServiceProxy;
37 import com.android.emailcommon.service.HostAuthCompat;
38 import com.android.emailcommon.utility.Utility;
39 import 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  */
52 public 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          */
onCheckSettingsComplete()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          */
onCheckSettingsSecurityRequired(String hostName)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          */
onCheckSettingsError(int reason, String message)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          */
onCheckSettingsAutoDiscoverComplete(int result)117         void onCheckSettingsAutoDiscoverComplete(int result);
118     }
119 
120     // Public no-args constructor needed for fragment re-instantiation
AccountCheckSettingsFragment()121     public AccountCheckSettingsFragment() {}
122 
123     /**
124      * Create a retained, invisible fragment that checks accounts
125      *
126      * @param mode incoming or outgoing
127      */
newInstance(int mode)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
onCreate(Bundle savedInstanceState)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
onActivityCreated(Bundle savedInstanceState)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
onResume()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
onPause()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
onDestroy()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
onDetach()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      */
reportProgress(int newState, MessagingException ex)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      */
getCallbackTarget()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          */
AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth)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          */
AccountCheckTask(Context context, AccountCheckSettingsFragment callback, int mode, SetupDataFragment setupData)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
doInBackground(Void... params)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
onProgressUpdate(Integer... progress)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
onPostExecute(MessagingException result)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      */
getProgressString(Context context, int progress)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      */
getProgressForMode(int checkMode)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