• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.AlertDialog;
21 import android.app.Dialog;
22 import android.app.DialogFragment;
23 import android.app.Fragment;
24 import android.app.FragmentManager;
25 import android.app.ProgressDialog;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.os.AsyncTask;
29 import android.os.Bundle;
30 import android.text.TextUtils;
31 import android.util.Log;
32 
33 import com.android.email.R;
34 import com.android.email.mail.Sender;
35 import com.android.email.mail.Store;
36 import com.android.emailcommon.Logging;
37 import com.android.emailcommon.mail.MessagingException;
38 import com.android.emailcommon.provider.Account;
39 import com.android.emailcommon.provider.HostAuth;
40 import com.android.emailcommon.provider.Policy;
41 import com.android.emailcommon.service.EmailServiceProxy;
42 import com.android.emailcommon.utility.Utility;
43 
44 /**
45  * Check incoming or outgoing settings, or perform autodiscovery.
46  *
47  * There are three components that work together.  1. This fragment is retained and non-displayed,
48  * and controls the overall process.  2. An AsyncTask that works with the stores/services to
49  * check the accounts settings.  3. A stateless progress dialog (which will be recreated on
50  * orientation changes).
51  *
52  * There are also two lightweight error dialogs which are used for notification of terminal
53  * conditions.
54  */
55 public class AccountCheckSettingsFragment extends Fragment {
56 
57     public final static String TAG = "AccountCheckSettingsFragment";
58 
59     // Debugging flags - for debugging the UI
60     // If true, use a "fake" account check cycle
61     private static final boolean DEBUG_FAKE_CHECK_CYCLE = false;            // DO NOT CHECK IN TRUE
62     // If true, use fake check cycle, return failure
63     private static final boolean DEBUG_FAKE_CHECK_ERR = false;              // DO NOT CHECK IN TRUE
64     // If true, use fake check cycle, return "security required"
65     private static final boolean DEBUG_FORCE_SECURITY_REQUIRED = false;     // DO NOT CHECK IN TRUE
66 
67     // State
68     private final static int STATE_START = 0;
69     private final static int STATE_CHECK_AUTODISCOVER = 1;
70     private final static int STATE_CHECK_INCOMING = 2;
71     private final static int STATE_CHECK_OUTGOING = 3;
72     private final static int STATE_CHECK_OK = 4;                    // terminal
73     private final static int STATE_CHECK_SHOW_SECURITY = 5;         // terminal
74     private final static int STATE_CHECK_ERROR = 6;                 // terminal
75     private final static int STATE_AUTODISCOVER_AUTH_DIALOG = 7;    // terminal
76     private final static int STATE_AUTODISCOVER_RESULT = 8;         // terminal
77     private int mState = STATE_START;
78 
79     // Support for UI
80     private boolean mAttached;
81     private boolean mPaused = false;
82     private CheckingDialog mCheckingDialog;
83     private MessagingException mProgressException;
84 
85     // Support for AsyncTask and account checking
86     AccountCheckTask mAccountCheckTask;
87 
88     // Result codes returned by onCheckSettingsComplete.
89     /** Check settings returned successfully */
90     public final static int CHECK_SETTINGS_OK = 0;
91     /** Check settings failed due to connection, authentication, or other server error */
92     public final static int CHECK_SETTINGS_SERVER_ERROR = 1;
93     /** Check settings failed due to user refusing to accept security requirements */
94     public final static int CHECK_SETTINGS_SECURITY_USER_DENY = 2;
95     /** Check settings failed due to certificate being required - user needs to pick immediately. */
96     public final static int CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED = 3;
97 
98     // Result codes returned by onAutoDiscoverComplete.
99     /** AutoDiscover completed successfully with server setup data */
100     public final static int AUTODISCOVER_OK = 0;
101     /** AutoDiscover completed with no data (no server or AD not supported) */
102     public final static int AUTODISCOVER_NO_DATA = 1;
103     /** AutoDiscover reported authentication error */
104     public final static int AUTODISCOVER_AUTHENTICATION = 2;
105 
106     /**
107      * Callback interface for any target or activity doing account check settings
108      */
109     public interface Callbacks {
110         /**
111          * Called when CheckSettings completed
112          * @param result check settings result code - success is CHECK_SETTINGS_OK
113          */
onCheckSettingsComplete(int result)114         public void onCheckSettingsComplete(int result);
115 
116         /**
117          * Called when autodiscovery completes.
118          * @param result autodiscovery result code - success is AUTODISCOVER_OK
119          * @param hostAuth configuration data returned by AD server, or null if no data available
120          */
onAutoDiscoverComplete(int result, HostAuth hostAuth)121         public void onAutoDiscoverComplete(int result, HostAuth hostAuth);
122     }
123 
124     /**
125      * Create a retained, invisible fragment that checks accounts
126      *
127      * @param mode incoming or outgoing
128      */
newInstance(int mode, Fragment parentFragment)129     public static AccountCheckSettingsFragment newInstance(int mode, Fragment parentFragment) {
130         AccountCheckSettingsFragment f = new AccountCheckSettingsFragment();
131         f.setTargetFragment(parentFragment, mode);
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
onCreate(Bundle savedInstanceState)140     public void onCreate(Bundle savedInstanceState) {
141         super.onCreate(savedInstanceState);
142         setRetainInstance(true);
143     }
144 
145     /**
146      * This is called when the Fragment's Activity is ready to go, after
147      * its content view has been installed; it is called both after
148      * the initial fragment creation and after the fragment is re-attached
149      * to a new activity.
150      */
151     @Override
onActivityCreated(Bundle savedInstanceState)152     public void onActivityCreated(Bundle savedInstanceState) {
153         super.onActivityCreated(savedInstanceState);
154         mAttached = true;
155 
156         // If this is the first time, start the AsyncTask
157         if (mAccountCheckTask == null) {
158             int checkMode = getTargetRequestCode();
159             Account checkAccount = SetupData.getAccount();
160             mAccountCheckTask = (AccountCheckTask)
161                     new AccountCheckTask(checkMode, checkAccount)
162                     .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
163         }
164     }
165 
166     /**
167      * When resuming, restart the progress/error UI if necessary by re-reporting previous values
168      */
169     @Override
onResume()170     public void onResume() {
171         super.onResume();
172         mPaused = false;
173         if (mState != STATE_START) {
174             reportProgress(mState, mProgressException);
175         }
176     }
177 
178     @Override
onPause()179     public void onPause() {
180         super.onPause();
181         mPaused = true;
182     }
183 
184     /**
185      * This is called when the fragment is going away.  It is NOT called
186      * when the fragment is being propagated between activity instances.
187      */
188     @Override
onDestroy()189     public void onDestroy() {
190         super.onDestroy();
191         if (mAccountCheckTask != null) {
192             Utility.cancelTaskInterrupt(mAccountCheckTask);
193             mAccountCheckTask = null;
194         }
195     }
196 
197     /**
198      * This is called right before the fragment is detached from its current activity instance.
199      * All reporting and callbacks are halted until we reattach.
200      */
201     @Override
onDetach()202     public void onDetach() {
203         super.onDetach();
204         mAttached = false;
205     }
206 
207     /**
208      * The worker (AsyncTask) will call this (in the UI thread) to report progress.  If we are
209      * attached to an activity, update the progress immediately;  If not, simply hold the
210      * progress for later.
211      * @param newState The new progress state being reported
212      */
reportProgress(int newState, MessagingException ex)213     private void reportProgress(int newState, MessagingException ex) {
214         mState = newState;
215         mProgressException = ex;
216 
217         // If we are attached, create, recover, and/or update the dialog
218         if (mAttached && !mPaused) {
219             FragmentManager fm = getFragmentManager();
220 
221             switch (newState) {
222                 case STATE_CHECK_OK:
223                     // immediately terminate, clean up, and report back
224                     // 1. get rid of progress dialog (if any)
225                     recoverAndDismissCheckingDialog();
226                     // 2. exit self
227                     fm.popBackStack();
228                     // 3. report OK back to target fragment or activity
229                     getCallbackTarget().onCheckSettingsComplete(CHECK_SETTINGS_OK);
230                     break;
231                 case STATE_CHECK_SHOW_SECURITY:
232                     // 1. get rid of progress dialog (if any)
233                     recoverAndDismissCheckingDialog();
234                     // 2. launch the error dialog, if needed
235                     if (fm.findFragmentByTag(SecurityRequiredDialog.TAG) == null) {
236                         String message = ex.getMessage();
237                         if (message != null) {
238                             message = message.trim();
239                         }
240                         SecurityRequiredDialog securityRequiredDialog =
241                                 SecurityRequiredDialog.newInstance(this, message);
242                         fm.beginTransaction()
243                                 .add(securityRequiredDialog, SecurityRequiredDialog.TAG)
244                                 .commit();
245                     }
246                     break;
247                 case STATE_CHECK_ERROR:
248                 case STATE_AUTODISCOVER_AUTH_DIALOG:
249                     // 1. get rid of progress dialog (if any)
250                     recoverAndDismissCheckingDialog();
251                     // 2. launch the error dialog, if needed
252                     if (fm.findFragmentByTag(ErrorDialog.TAG) == null) {
253                         ErrorDialog errorDialog = ErrorDialog.newInstance(
254                                 getActivity(), this, mProgressException);
255                         fm.beginTransaction()
256                                 .add(errorDialog, ErrorDialog.TAG)
257                                 .commit();
258                     }
259                     break;
260                 case STATE_AUTODISCOVER_RESULT:
261                     HostAuth autoDiscoverResult = ((AutoDiscoverResults) ex).mHostAuth;
262                     // 1. get rid of progress dialog (if any)
263                     recoverAndDismissCheckingDialog();
264                     // 2. exit self
265                     fm.popBackStack();
266                     // 3. report back to target fragment or activity
267                     getCallbackTarget().onAutoDiscoverComplete(
268                             (autoDiscoverResult != null) ? AUTODISCOVER_OK : AUTODISCOVER_NO_DATA,
269                             autoDiscoverResult);
270                     break;
271                 default:
272                     // Display a normal progress message
273                     mCheckingDialog = (CheckingDialog) fm.findFragmentByTag(CheckingDialog.TAG);
274 
275                     if (mCheckingDialog == null) {
276                         mCheckingDialog = CheckingDialog.newInstance(this, mState);
277                         fm.beginTransaction()
278                                 .add(mCheckingDialog, CheckingDialog.TAG)
279                                 .commit();
280                     } else {
281                         mCheckingDialog.updateProgress(mState);
282                     }
283                     break;
284             }
285         }
286     }
287 
288     /**
289      * Find the callback target, either a target fragment or the activity
290      */
getCallbackTarget()291     private Callbacks getCallbackTarget() {
292         Fragment target = getTargetFragment();
293         if (target instanceof Callbacks) {
294             return (Callbacks) target;
295         }
296         Activity activity = getActivity();
297         if (activity instanceof Callbacks) {
298             return (Callbacks) activity;
299         }
300         throw new IllegalStateException();
301     }
302 
303     /**
304      * Recover and dismiss the progress dialog fragment
305      */
recoverAndDismissCheckingDialog()306     private void recoverAndDismissCheckingDialog() {
307         if (mCheckingDialog == null) {
308             mCheckingDialog = (CheckingDialog)
309                     getFragmentManager().findFragmentByTag(CheckingDialog.TAG);
310         }
311         if (mCheckingDialog != null) {
312             mCheckingDialog.dismissAllowingStateLoss();
313             mCheckingDialog = null;
314         }
315     }
316 
317     /**
318      * This is called when the user clicks "cancel" on the progress dialog.  Shuts everything
319      * down and dismisses everything.
320      * This should cause us to remain in the current screen (not accepting the settings)
321      */
onCheckingDialogCancel()322     private void onCheckingDialogCancel() {
323         // 1. kill the checker
324         Utility.cancelTaskInterrupt(mAccountCheckTask);
325         mAccountCheckTask = null;
326         // 2. kill self with no report - this is "cancel"
327         finish();
328     }
329 
onEditCertificateOk()330     private void onEditCertificateOk() {
331         Callbacks callbackTarget = getCallbackTarget();
332         getCallbackTarget().onCheckSettingsComplete(CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED);
333         finish();
334     }
335 
336     /**
337      * This is called when the user clicks "edit" from the error dialog.  The error dialog
338      * should have already dismissed itself.
339      * Depending on the context, the target will remain in the current activity (e.g. editing
340      * settings) or return to its own parent (e.g. enter new credentials).
341      */
onErrorDialogEditButton()342     private void onErrorDialogEditButton() {
343         // 1. handle "edit" - notify callback that we had a problem with the test
344         Callbacks callbackTarget = getCallbackTarget();
345         if (mState == STATE_AUTODISCOVER_AUTH_DIALOG) {
346             // report auth error to target fragment or activity
347             callbackTarget.onAutoDiscoverComplete(AUTODISCOVER_AUTHENTICATION, null);
348         } else {
349             // report check settings failure to target fragment or activity
350             callbackTarget.onCheckSettingsComplete(CHECK_SETTINGS_SERVER_ERROR);
351         }
352         finish();
353     }
354 
355     /** Kill self if not already killed. */
finish()356     private void finish() {
357         FragmentManager fm = getFragmentManager();
358         if (fm != null) {
359             fm.popBackStack();
360         }
361     }
362 
363     /**
364      * This is called when the user clicks "ok" or "cancel" on the "security required" dialog.
365      * Shuts everything down and dismisses everything, and reports the result appropriately.
366      */
onSecurityRequiredDialogResultOk(boolean okPressed)367     private void onSecurityRequiredDialogResultOk(boolean okPressed) {
368         // 1. handle OK/cancel - notify that security is OK and we can proceed
369         Callbacks callbackTarget = getCallbackTarget();
370         callbackTarget.onCheckSettingsComplete(
371                 okPressed ? CHECK_SETTINGS_OK : CHECK_SETTINGS_SECURITY_USER_DENY);
372 
373         // 2. kill self if not already killed by callback
374         FragmentManager fm = getFragmentManager();
375         if (fm != null) {
376             fm.popBackStack();
377         }
378     }
379 
380     /**
381      * This exception class is used to report autodiscover results via the reporting mechanism.
382      */
383     public static class AutoDiscoverResults extends MessagingException {
384         public final HostAuth mHostAuth;
385 
386         /**
387          * @param authenticationError true if auth failure, false for result (or no response)
388          * @param hostAuth null for "no autodiscover", non-null for server info to return
389          */
AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth)390         public AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth) {
391             super(null);
392             if (authenticationError) {
393                 mExceptionType = AUTODISCOVER_AUTHENTICATION_FAILED;
394             } else {
395                 mExceptionType = AUTODISCOVER_AUTHENTICATION_RESULT;
396             }
397             mHostAuth = hostAuth;
398         }
399     }
400 
401     /**
402      * This AsyncTask does the actual account checking
403      *
404      * TODO: It would be better to remove the UI complete from here (the exception->string
405      * conversions).
406      */
407     private class AccountCheckTask extends AsyncTask<Void, Integer, MessagingException> {
408 
409         final Context mContext;
410         final int mMode;
411         final Account mAccount;
412         final String mStoreHost;
413         final String mCheckEmail;
414         final String mCheckPassword;
415 
416         /**
417          * Create task and parameterize it
418          * @param mode bits request operations
419          * @param checkAccount account holding values to be checked
420          */
AccountCheckTask(int mode, Account checkAccount)421         public AccountCheckTask(int mode, Account checkAccount) {
422             mContext = getActivity().getApplicationContext();
423             mMode = mode;
424             mAccount = checkAccount;
425             mStoreHost = checkAccount.mHostAuthRecv.mAddress;
426             mCheckEmail = checkAccount.mEmailAddress;
427             mCheckPassword = checkAccount.mHostAuthRecv.mPassword;
428         }
429 
430         @Override
doInBackground(Void... params)431         protected MessagingException doInBackground(Void... params) {
432             if (DEBUG_FAKE_CHECK_CYCLE) {
433                 return fakeChecker();
434             }
435 
436             try {
437                 if ((mMode & SetupData.CHECK_AUTODISCOVER) != 0) {
438                     if (isCancelled()) return null;
439                     publishProgress(STATE_CHECK_AUTODISCOVER);
440                     Log.d(Logging.LOG_TAG, "Begin auto-discover for " + mCheckEmail);
441                     Store store = Store.getInstance(mAccount, mContext);
442                     Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword);
443                     // Result will be one of:
444                     //  null: remote exception - proceed to manual setup
445                     //  MessagingException.AUTHENTICATION_FAILED: username/password rejected
446                     //  Other error: proceed to manual setup
447                     //  No error: return autodiscover results
448                     if (result == null) {
449                         return new AutoDiscoverResults(false, null);
450                     }
451                     int errorCode =
452                             result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
453                     if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) {
454                         return new AutoDiscoverResults(true, null);
455                     } else if (errorCode != MessagingException.NO_ERROR) {
456                         return new AutoDiscoverResults(false, null);
457                     } else {
458                         HostAuth serverInfo = (HostAuth)
459                             result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH);
460                         return new AutoDiscoverResults(false, serverInfo);
461                     }
462                 }
463 
464                 // Check Incoming Settings
465                 if ((mMode & SetupData.CHECK_INCOMING) != 0) {
466                     if (isCancelled()) return null;
467                     Log.d(Logging.LOG_TAG, "Begin check of incoming email settings");
468                     publishProgress(STATE_CHECK_INCOMING);
469                     Store store = Store.getInstance(mAccount, mContext);
470                     Bundle bundle = store.checkSettings();
471                     int resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
472                     if (bundle != null) {
473                         resultCode = bundle.getInt(
474                                 EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE);
475                     }
476                     if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) {
477                         SetupData.setPolicy((Policy)bundle.getParcelable(
478                                 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET));
479                         return new MessagingException(resultCode, mStoreHost);
480                     } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) {
481                         String[] data = bundle.getStringArray(
482                                 EmailServiceProxy.VALIDATE_BUNDLE_UNSUPPORTED_POLICIES);
483                         return new MessagingException(resultCode, mStoreHost, data);
484                     } else if (resultCode != MessagingException.NO_ERROR) {
485                         String errorMessage =
486                             bundle.getString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
487                         return new MessagingException(resultCode, errorMessage);
488                     }
489                 }
490 
491                 // Check Outgoing Settings
492                 if ((mMode & SetupData.CHECK_OUTGOING) != 0) {
493                     if (isCancelled()) return null;
494                     Log.d(Logging.LOG_TAG, "Begin check of outgoing email settings");
495                     publishProgress(STATE_CHECK_OUTGOING);
496                     Sender sender = Sender.getInstance(mContext, mAccount);
497                     sender.close();
498                     sender.open();
499                     sender.close();
500                 }
501 
502                 // If we reached the end, we completed the check(s) successfully
503                 return null;
504             } catch (final MessagingException me) {
505                 // Some of the legacy account checkers return errors by throwing MessagingException,
506                 // which we catch and return here.
507                 return me;
508             }
509         }
510 
511         /**
512          * Dummy background worker, for testing UI only.
513          */
fakeChecker()514         private MessagingException fakeChecker() {
515             // Dummy:  Publish a series of progress setups, 2 sec delays between them;
516             // then return "ok" (null)
517             final int DELAY = 2*1000;
518             if (isCancelled()) return null;
519             if ((mMode & SetupData.CHECK_AUTODISCOVER) != 0) {
520                 publishProgress(STATE_CHECK_AUTODISCOVER);
521                 try {
522                     Thread.sleep(DELAY);
523                 } catch (InterruptedException e) { }
524                 if (DEBUG_FAKE_CHECK_ERR) {
525                     return new MessagingException(MessagingException.AUTHENTICATION_FAILED);
526                 }
527                 // Return "real" AD results
528                 HostAuth auth = new HostAuth();
529                 auth.setLogin("user", "password");
530                 auth.setConnection(HostAuth.SCHEME_EAS, "testserver.com", 0);
531                 return new AutoDiscoverResults(false, auth);
532             }
533             if (isCancelled()) return null;
534             if ((mMode & SetupData.CHECK_INCOMING) != 0) {
535                 publishProgress(STATE_CHECK_INCOMING);
536                 try {
537                     Thread.sleep(DELAY);
538                 } catch (InterruptedException e) { }
539                 if (DEBUG_FAKE_CHECK_ERR) {
540                     return new MessagingException(MessagingException.IOERROR);
541                 } else if (DEBUG_FORCE_SECURITY_REQUIRED) {
542                     return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
543                 }
544             }
545             if (isCancelled()) return null;
546             if ((mMode & SetupData.CHECK_OUTGOING) != 0) {
547                 publishProgress(STATE_CHECK_OUTGOING);
548                 try {
549                     Thread.sleep(DELAY);
550                 } catch (InterruptedException e) { }
551                 if (DEBUG_FAKE_CHECK_ERR) {
552                     return new MessagingException(MessagingException.TLS_REQUIRED);
553                 }
554             }
555             return null;
556         }
557 
558         /**
559          * Progress reports (runs in UI thread).  This should be used for real progress only
560          * (not for errors).
561          */
562         @Override
onProgressUpdate(Integer... progress)563         protected void onProgressUpdate(Integer... progress) {
564             if (isCancelled()) return;
565             reportProgress(progress[0], null);
566         }
567 
568         /**
569          * Result handler (runs in UI thread).
570          *
571          * AutoDiscover authentication errors are handled a bit differently than the
572          * other errors;  If encountered, we display the error dialog, but we return with
573          * a different callback used only for AutoDiscover.
574          *
575          * @param result null for a successful check;  exception for various errors
576          */
577         @Override
onPostExecute(MessagingException result)578         protected void onPostExecute(MessagingException result) {
579             if (isCancelled()) return;
580             if (result == null) {
581                 reportProgress(STATE_CHECK_OK, null);
582             } else {
583                 int progressState = STATE_CHECK_ERROR;
584                 int exceptionType = result.getExceptionType();
585 
586                 switch (exceptionType) {
587                     // NOTE: AutoDiscover reports have their own reporting state, handle differently
588                     // from the other exception types
589                     case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
590                         progressState = STATE_AUTODISCOVER_AUTH_DIALOG;
591                         break;
592                     case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT:
593                         progressState = STATE_AUTODISCOVER_RESULT;
594                         break;
595                     // NOTE: Security policies required has its own report state, handle it a bit
596                     // differently from the other exception types.
597                     case MessagingException.SECURITY_POLICIES_REQUIRED:
598                         progressState = STATE_CHECK_SHOW_SECURITY;
599                         break;
600                 }
601                 reportProgress(progressState, result);
602             }
603         }
604     }
605 
getErrorString(Context context, MessagingException ex)606     private static String getErrorString(Context context, MessagingException ex) {
607         int id;
608         String message = ex.getMessage();
609         if (message != null) {
610             message = message.trim();
611         }
612         switch (ex.getExceptionType()) {
613             // The remaining exception types are handled by setting the state to
614             // STATE_CHECK_ERROR (above, default) and conversion to specific error strings.
615             case MessagingException.CERTIFICATE_VALIDATION_ERROR:
616                 id = TextUtils.isEmpty(message)
617                         ? R.string.account_setup_failed_dlg_certificate_message
618                         : R.string.account_setup_failed_dlg_certificate_message_fmt;
619                 break;
620             case MessagingException.AUTHENTICATION_FAILED:
621             case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
622                 id = TextUtils.isEmpty(message)
623                         ? R.string.account_setup_failed_dlg_auth_message
624                         : R.string.account_setup_failed_dlg_auth_message_fmt;
625                 break;
626             case MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR:
627                 id = R.string.account_setup_failed_check_credentials_message;
628                 break;
629             case MessagingException.IOERROR:
630                 id = R.string.account_setup_failed_ioerror;
631                 break;
632             case MessagingException.TLS_REQUIRED:
633                 id = R.string.account_setup_failed_tls_required;
634                 break;
635             case MessagingException.AUTH_REQUIRED:
636                 id = R.string.account_setup_failed_auth_required;
637                 break;
638             case MessagingException.SECURITY_POLICIES_UNSUPPORTED:
639                 id = R.string.account_setup_failed_security_policies_unsupported;
640                 // Belt and suspenders here; there should always be a non-empty array here
641                 String[] unsupportedPolicies = (String[]) ex.getExceptionData();
642                 if (unsupportedPolicies == null) {
643                     Log.w(TAG, "No data for unsupported policies?");
644                     break;
645                 }
646                 // Build a string, concatenating policies we don't support
647                 StringBuilder sb = new StringBuilder();
648                 boolean first = true;
649                 for (String policyName: unsupportedPolicies) {
650                     if (first) {
651                         first = false;
652                     } else {
653                         sb.append(", ");
654                     }
655                     sb.append(policyName);
656                 }
657                 message = sb.toString();
658                 break;
659             case MessagingException.ACCESS_DENIED:
660                 id = R.string.account_setup_failed_access_denied;
661                 break;
662             case MessagingException.PROTOCOL_VERSION_UNSUPPORTED:
663                 id = R.string.account_setup_failed_protocol_unsupported;
664                 break;
665             case MessagingException.GENERAL_SECURITY:
666                 id = R.string.account_setup_failed_security;
667                 break;
668             case MessagingException.CLIENT_CERTIFICATE_REQUIRED:
669                 id = R.string.account_setup_failed_certificate_required;
670                 break;
671             case MessagingException.CLIENT_CERTIFICATE_ERROR:
672                 id = R.string.account_setup_failed_certificate_inaccessible;
673                 break;
674             default:
675                 id = TextUtils.isEmpty(message)
676                         ? R.string.account_setup_failed_dlg_server_message
677                         : R.string.account_setup_failed_dlg_server_message_fmt;
678                 break;
679         }
680         return TextUtils.isEmpty(message)
681                 ? context.getString(id)
682                 : context.getString(id, message);
683     }
684 
685     /**
686      * Simple dialog that shows progress as we work through the settings checks.
687      * This is stateless except for its UI (e.g. current strings) and can be torn down or
688      * recreated at any time without affecting the account checking progress.
689      */
690     public static class CheckingDialog extends DialogFragment {
691         @SuppressWarnings("hiding")
692         public final static String TAG = "CheckProgressDialog";
693 
694         // Extras for saved instance state
695         private final String EXTRA_PROGRESS_STRING = "CheckProgressDialog.Progress";
696 
697         // UI
698         private String mProgressString;
699 
700         /**
701          * Create a dialog that reports progress
702          * @param progress initial progress indication
703          */
newInstance(AccountCheckSettingsFragment parentFragment, int progress)704         public static CheckingDialog newInstance(AccountCheckSettingsFragment parentFragment,
705                 int progress) {
706             CheckingDialog f = new CheckingDialog();
707             f.setTargetFragment(parentFragment, progress);
708             return f;
709         }
710 
711         /**
712          * Update the progress of an existing dialog
713          * @param progress latest progress to be displayed
714          */
updateProgress(int progress)715         public void updateProgress(int progress) {
716             mProgressString = getProgressString(progress);
717             AlertDialog dialog = (AlertDialog) getDialog();
718             if (dialog != null && mProgressString != null) {
719                 dialog.setMessage(mProgressString);
720             }
721         }
722 
723         @Override
onCreateDialog(Bundle savedInstanceState)724         public Dialog onCreateDialog(Bundle savedInstanceState) {
725             Context context = getActivity();
726             if (savedInstanceState != null) {
727                 mProgressString = savedInstanceState.getString(EXTRA_PROGRESS_STRING);
728             }
729             if (mProgressString == null) {
730                 mProgressString = getProgressString(getTargetRequestCode());
731             }
732             final AccountCheckSettingsFragment target =
733                 (AccountCheckSettingsFragment) getTargetFragment();
734 
735             ProgressDialog dialog = new ProgressDialog(context);
736             dialog.setIndeterminate(true);
737             dialog.setMessage(mProgressString);
738             dialog.setButton(DialogInterface.BUTTON_NEGATIVE,
739                     context.getString(R.string.cancel_action),
740                     new DialogInterface.OnClickListener() {
741                         @Override
742                         public void onClick(DialogInterface dialog, int which) {
743                             dismiss();
744                             target.onCheckingDialogCancel();
745                         }
746                     });
747             return dialog;
748         }
749 
750         /**
751          * Listen for cancellation, which can happen from places other than the
752          * negative button (e.g. touching outside the dialog), and stop the checker
753          */
754         @Override
onCancel(DialogInterface dialog)755         public void onCancel(DialogInterface dialog) {
756             AccountCheckSettingsFragment target =
757                 (AccountCheckSettingsFragment) getTargetFragment();
758             target.onCheckingDialogCancel();
759             super.onCancel(dialog);
760         }
761 
762         @Override
onSaveInstanceState(Bundle outState)763         public void onSaveInstanceState(Bundle outState) {
764             super.onSaveInstanceState(outState);
765             outState.putString(EXTRA_PROGRESS_STRING, mProgressString);
766         }
767 
768         /**
769          * Convert progress to message
770          */
getProgressString(int progress)771         private String getProgressString(int progress) {
772             int stringId = 0;
773             switch (progress) {
774                 case STATE_CHECK_AUTODISCOVER:
775                     stringId = R.string.account_setup_check_settings_retr_info_msg;
776                     break;
777                 case STATE_CHECK_INCOMING:
778                     stringId = R.string.account_setup_check_settings_check_incoming_msg;
779                     break;
780                 case STATE_CHECK_OUTGOING:
781                     stringId = R.string.account_setup_check_settings_check_outgoing_msg;
782                     break;
783             }
784             return getActivity().getString(stringId);
785         }
786     }
787 
788     /**
789      * The standard error dialog.  Calls back to onErrorDialogButton().
790      */
791     public static class ErrorDialog extends DialogFragment {
792         @SuppressWarnings("hiding")
793         public final static String TAG = "ErrorDialog";
794 
795         // Bundle keys for arguments
796         private final static String ARGS_MESSAGE = "ErrorDialog.Message";
797         private final static String ARGS_EXCEPTION_ID = "ErrorDialog.ExceptionId";
798 
799         /**
800          * Use {@link #newInstance} This public constructor is still required so
801          * that DialogFragment state can be automatically restored by the
802          * framework.
803          */
ErrorDialog()804         public ErrorDialog() {
805         }
806 
newInstance(Context context, AccountCheckSettingsFragment target, MessagingException ex)807         public static ErrorDialog newInstance(Context context, AccountCheckSettingsFragment target,
808                 MessagingException ex) {
809             ErrorDialog fragment = new ErrorDialog();
810             Bundle arguments = new Bundle();
811             arguments.putString(ARGS_MESSAGE, getErrorString(context, ex));
812             arguments.putInt(ARGS_EXCEPTION_ID, ex.getExceptionType());
813             fragment.setArguments(arguments);
814             fragment.setTargetFragment(target, 0);
815             return fragment;
816         }
817 
818         @Override
onCreateDialog(Bundle savedInstanceState)819         public Dialog onCreateDialog(Bundle savedInstanceState) {
820             final Context context = getActivity();
821             final Bundle arguments = getArguments();
822             final String message = arguments.getString(ARGS_MESSAGE);
823             final int exceptionId = arguments.getInt(ARGS_EXCEPTION_ID);
824             final AccountCheckSettingsFragment target =
825                     (AccountCheckSettingsFragment) getTargetFragment();
826 
827             AlertDialog.Builder builder = new AlertDialog.Builder(context)
828                 .setIconAttribute(android.R.attr.alertDialogIcon)
829                 .setTitle(context.getString(R.string.account_setup_failed_dlg_title))
830                 .setMessage(message)
831                 .setCancelable(true);
832 
833             if (exceptionId == MessagingException.CLIENT_CERTIFICATE_REQUIRED) {
834                 // Certificate error - show two buttons so the host fragment can auto pop
835                 // into the appropriate flow.
836                 builder.setPositiveButton(
837                         context.getString(android.R.string.ok),
838                         new DialogInterface.OnClickListener() {
839                             @Override
840                             public void onClick(DialogInterface dialog, int which) {
841                                 dismiss();
842                                 target.onEditCertificateOk();
843                             }
844                         });
845                 builder.setNegativeButton(
846                         context.getString(android.R.string.cancel),
847                         new DialogInterface.OnClickListener() {
848                             @Override
849                             public void onClick(DialogInterface dialog, int which) {
850                                 dismiss();
851                                 target.onErrorDialogEditButton();
852                             }
853                         });
854 
855             } else {
856                 // "Normal" error - just use a single "Edit details" button.
857                 builder.setPositiveButton(
858                         context.getString(R.string.account_setup_failed_dlg_edit_details_action),
859                         new DialogInterface.OnClickListener() {
860                             @Override
861                             public void onClick(DialogInterface dialog, int which) {
862                                 dismiss();
863                                 target.onErrorDialogEditButton();
864                             }
865                         });
866             }
867 
868             return builder.create();
869         }
870 
871     }
872 
873     /**
874      * The "security required" error dialog.  This is presented whenever an exchange account
875      * reports that it will require security policy control, and provide the user with the
876      * opportunity to accept or deny this.
877      *
878      * If the user clicks OK, calls onSecurityRequiredDialogResultOk(true) which reports back
879      * to the target as if the settings check was "ok".  If the user clicks "cancel", calls
880      * onSecurityRequiredDialogResultOk(false) which simply closes the checker (this is the
881      * same as any other failed check.)
882      */
883     public static class SecurityRequiredDialog extends DialogFragment {
884         @SuppressWarnings("hiding")
885         public final static String TAG = "SecurityRequiredDialog";
886 
887         // Bundle keys for arguments
888         private final static String ARGS_HOST_NAME = "SecurityRequiredDialog.HostName";
889 
newInstance(AccountCheckSettingsFragment target, String hostName)890         public static SecurityRequiredDialog newInstance(AccountCheckSettingsFragment target,
891                 String hostName) {
892             SecurityRequiredDialog fragment = new SecurityRequiredDialog();
893             Bundle arguments = new Bundle();
894             arguments.putString(ARGS_HOST_NAME, hostName);
895             fragment.setArguments(arguments);
896             fragment.setTargetFragment(target, 0);
897             return fragment;
898         }
899 
900         @Override
onCreateDialog(Bundle savedInstanceState)901         public Dialog onCreateDialog(Bundle savedInstanceState) {
902             final Context context = getActivity();
903             final Bundle arguments = getArguments();
904             final String hostName = arguments.getString(ARGS_HOST_NAME);
905             final AccountCheckSettingsFragment target =
906                     (AccountCheckSettingsFragment) getTargetFragment();
907 
908             return new AlertDialog.Builder(context)
909                 .setIconAttribute(android.R.attr.alertDialogIcon)
910                 .setTitle(context.getString(R.string.account_setup_security_required_title))
911                 .setMessage(context.getString(
912                         R.string.account_setup_security_policies_required_fmt, hostName))
913                 .setCancelable(true)
914                 .setPositiveButton(
915                         context.getString(R.string.okay_action),
916                         new DialogInterface.OnClickListener() {
917                             @Override
918                             public void onClick(DialogInterface dialog, int which) {
919                                 dismiss();
920                                 target.onSecurityRequiredDialogResultOk(true);
921                             }
922                         })
923                 .setNegativeButton(
924                         context.getString(R.string.cancel_action),
925                         new DialogInterface.OnClickListener() {
926                             @Override
927                             public void onClick(DialogInterface dialog, int which) {
928                                 dismiss();
929                                 target.onSecurityRequiredDialogResultOk(false);
930                             }
931                         })
932                  .create();
933         }
934 
935     }
936 
937 }
938