Home | History | Annotate | Download | only in setup
      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.content.Context;
     22 import android.os.AsyncTask;
     23 import android.os.Bundle;
     24 import android.view.KeyEvent;
     25 import android.view.View;
     26 import android.view.View.OnClickListener;
     27 import android.view.View.OnFocusChangeListener;
     28 import android.view.inputmethod.EditorInfo;
     29 import android.view.inputmethod.InputMethodManager;
     30 import android.widget.Button;
     31 import android.widget.TextView;
     32 import android.widget.TextView.OnEditorActionListener;
     33 
     34 import com.android.email.R;
     35 import com.android.email.activity.UiUtilities;
     36 import com.android.emailcommon.provider.Account;
     37 import com.android.emailcommon.provider.HostAuth;
     38 import com.android.emailcommon.utility.Utility;
     39 
     40 import java.net.URI;
     41 import java.net.URISyntaxException;
     42 
     43 /**
     44  * Common base class for server settings fragments, so they can be more easily manipulated by
     45  * AccountSettingsXL.  Provides the following common functionality:
     46  *
     47  * Activity-provided callbacks
     48  * Activity callback during onAttach
     49  * Present "Next" button and respond to its clicks
     50  */
     51 public abstract class AccountServerBaseFragment extends Fragment
     52         implements AccountCheckSettingsFragment.Callbacks, OnClickListener {
     53 
     54     public static Bundle sSetupModeArgs = null;
     55     protected static URI sDefaultUri;
     56 
     57     private static final String BUNDLE_KEY_SETTINGS = "AccountServerBaseFragment.settings";
     58     private static final String BUNDLE_KEY_ACTIVITY_TITLE = "AccountServerBaseFragment.title";
     59 
     60     protected Context mContext;
     61     protected Callback mCallback = EmptyCallback.INSTANCE;
     62     /**
     63      * Whether or not we are in "settings mode". We re-use the same screens for both the initial
     64      * account creation as well as subsequent account modification. If <code>mSettingsMode</code>
     65      * if <code>false</code>, we are in account creation mode. Otherwise, we are in account
     66      * modification mode.
     67      */
     68     protected boolean mSettingsMode;
     69     /*package*/ HostAuth mLoadedSendAuth;
     70     /*package*/ HostAuth mLoadedRecvAuth;
     71 
     72     // This is null in the setup wizard screens, and non-null in AccountSettings mode
     73     private Button mProceedButton;
     74     // This is used to debounce multiple clicks on the proceed button (which does async work)
     75     private boolean mProceedButtonPressed;
     76     /*package*/ String mBaseScheme = "protocol";
     77 
     78     /**
     79      * Callback interface that owning activities must provide
     80      */
     81     public interface Callback {
     82         /**
     83          * Called each time the user-entered input transitions between valid and invalid
     84          * @param enable true to enable proceed/next button, false to disable
     85          */
     86         public void onEnableProceedButtons(boolean enable);
     87 
     88         /**
     89          * Called when user clicks "next".  Starts account checker.
     90          * @param checkMode values from {@link SetupData}
     91          * @param target the fragment that requested the check
     92          */
     93         public void onProceedNext(int checkMode, AccountServerBaseFragment target);
     94 
     95         /**
     96          * Called when account checker completes.  Fragments are responsible for saving
     97          * own edited data;  This is primarily for the activity to do post-check navigation.
     98          * @param result check settings result code - success is CHECK_SETTINGS_OK
     99          * @param setupMode signals if we were editing or creating
    100          */
    101         public void onCheckSettingsComplete(int result, int setupMode);
    102     }
    103 
    104     private static class EmptyCallback implements Callback {
    105         public static final Callback INSTANCE = new EmptyCallback();
    106         @Override public void onEnableProceedButtons(boolean enable) { }
    107         @Override public void onProceedNext(int checkMode, AccountServerBaseFragment target) { }
    108         @Override public void onCheckSettingsComplete(int result, int setupMode) { }
    109     }
    110 
    111     /**
    112      * Get the static arguments bundle that forces a server settings fragment into "settings" mode
    113      * (If not included, you'll be in "setup" mode which behaves slightly differently.)
    114      */
    115     public static synchronized Bundle getSettingsModeArgs() {
    116         if (sSetupModeArgs == null) {
    117             sSetupModeArgs = new Bundle();
    118             sSetupModeArgs.putBoolean(BUNDLE_KEY_SETTINGS, true);
    119         }
    120         return sSetupModeArgs;
    121     }
    122 
    123     public AccountServerBaseFragment() {
    124         if (sDefaultUri == null) {
    125             try {
    126                 sDefaultUri = new URI("");
    127             } catch (URISyntaxException ignore) {
    128                 // ignore; will never happen
    129             }
    130         }
    131     }
    132 
    133     /**
    134      * At onCreate time, read the fragment arguments
    135      */
    136     @Override
    137     public void onCreate(Bundle savedInstanceState) {
    138         super.onCreate(savedInstanceState);
    139 
    140         // Get arguments, which modally switch us into "settings" mode (different appearance)
    141         mSettingsMode = false;
    142         if (getArguments() != null) {
    143             mSettingsMode = getArguments().getBoolean(BUNDLE_KEY_SETTINGS);
    144         }
    145     }
    146 
    147     /**
    148      * Called from onCreateView, to do settings mode configuration
    149      */
    150     protected void onCreateViewSettingsMode(View view) {
    151         if (mSettingsMode) {
    152             UiUtilities.getView(view, R.id.cancel).setOnClickListener(this);
    153             mProceedButton = (Button) UiUtilities.getView(view, R.id.done);
    154             mProceedButton.setOnClickListener(this);
    155             mProceedButton.setEnabled(false);
    156         }
    157     }
    158 
    159     @Override
    160     public void onActivityCreated(Bundle savedInstanceState) {
    161         // startPreferencePanel launches this fragment with the right title initially, but
    162         // if the device is rotate we must set the title ourselves
    163         if (mSettingsMode && savedInstanceState != null) {
    164             getActivity().setTitle(savedInstanceState.getString(BUNDLE_KEY_ACTIVITY_TITLE));
    165         }
    166         super.onActivityCreated(savedInstanceState);
    167     }
    168 
    169     @Override
    170     public void onSaveInstanceState(Bundle outState) {
    171         outState.putString(BUNDLE_KEY_ACTIVITY_TITLE, (String) getActivity().getTitle());
    172     }
    173 
    174     @Override
    175     public void onAttach(Activity activity) {
    176         super.onAttach(activity);
    177         mContext = activity;
    178     }
    179 
    180     @Override
    181     public void onDetach() {
    182         super.onDetach();
    183 
    184         // Ensure that we don't have any callbacks at this point.
    185         mCallback = EmptyCallback.INSTANCE;
    186     }
    187 
    188     @Override
    189     public void onPause() {
    190         // Hide the soft keyboard if we lose focus
    191         InputMethodManager imm =
    192                 (InputMethodManager)mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
    193         imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
    194         super.onPause();
    195     }
    196 
    197     /**
    198      * Implements OnClickListener
    199      */
    200     @Override
    201     public void onClick(View v) {
    202         switch (v.getId()) {
    203             case R.id.cancel:
    204                 getActivity().onBackPressed();
    205                 break;
    206             case R.id.done:
    207                 // Simple debounce - just ignore while checks are underway
    208                 if (mProceedButtonPressed) {
    209                     return;
    210                 }
    211                 mProceedButtonPressed = true;
    212                 onNext();
    213                 break;
    214         }
    215     }
    216 
    217     /**
    218      * Activity provides callbacks here.
    219      */
    220     public void setCallback(Callback callback) {
    221         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
    222         mContext = getActivity();
    223     }
    224 
    225     /**
    226      * Enable/disable the "next" button
    227      */
    228     public void enableNextButton(boolean enable) {
    229         // If we are in settings "mode" we may be showing our own next button, and we'll
    230         // enable it directly, here
    231         if (mProceedButton != null) {
    232             mProceedButton.setEnabled(enable);
    233         }
    234         clearButtonBounce();
    235 
    236         // TODO: This supports the phone UX activities and will be removed
    237         mCallback.onEnableProceedButtons(enable);
    238     }
    239 
    240     /**
    241      * Performs async operations as part of saving changes to the settings.
    242      *      Check for duplicate account
    243      *      Display dialog if necessary
    244      *      Else, proceed via mCallback.onProceedNext
    245      */
    246     protected void startDuplicateTaskCheck(long accountId, String checkHost, String checkLogin,
    247             int checkSettingsMode) {
    248         new DuplicateCheckTask(accountId, checkHost, checkLogin, checkSettingsMode)
    249                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    250     }
    251 
    252     /**
    253      * Make the given text view uneditable. If the text view is ever focused, the specified
    254      * error message will be displayed.
    255      */
    256     protected void makeTextViewUneditable(final TextView view, final String errorMessage) {
    257         // We're editing an existing account; don't allow modification of the user name
    258         if (mSettingsMode) {
    259             view.setKeyListener(null);
    260             view.setFocusable(true);
    261             view.setOnFocusChangeListener(new OnFocusChangeListener() {
    262                 @Override
    263                 public void onFocusChange(View v, boolean hasFocus) {
    264                     if (hasFocus) {
    265                         // Framework will not auto-hide IME; do it ourselves
    266                         InputMethodManager imm = (InputMethodManager)mContext.
    267                                 getSystemService(Context.INPUT_METHOD_SERVICE);
    268                         imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
    269                         view.setError(errorMessage);
    270                     } else {
    271                         view.setError(null);
    272                     }
    273                 }
    274             });
    275             view.setOnClickListener(new OnClickListener() {
    276                 @Override
    277                 public void onClick(View v) {
    278                     if (view.getError() == null) {
    279                         view.setError(errorMessage);
    280                     } else {
    281                         view.setError(null);
    282                     }
    283                 }
    284             });
    285         }
    286     }
    287 
    288     /**
    289      * A keyboard listener which dismisses the keyboard when "DONE" is pressed, but doesn't muck
    290      * around with focus. This is useful in settings screens, as we don't want focus to change
    291      * since some fields throw up errors when they're focused to give the user more info.
    292      */
    293     protected final OnEditorActionListener mDismissImeOnDoneListener =
    294             new OnEditorActionListener() {
    295         @Override
    296         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
    297             if (actionId == EditorInfo.IME_ACTION_DONE) {
    298                 // Dismiss soft keyboard but don't modify focus.
    299                 final Context context = getActivity();
    300                 if (context == null) {
    301                     return false;
    302                 }
    303                 InputMethodManager imm = (InputMethodManager) context.getSystemService(
    304                         Context.INPUT_METHOD_SERVICE);
    305                 if (imm != null && imm.isActive()) {
    306                     imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
    307                 }
    308                 return true;
    309             }
    310             return false;
    311         }
    312     };
    313 
    314     /**
    315      * Clears the "next" button de-bounce flags and allows the "next" button to activate.
    316      */
    317     private void clearButtonBounce() {
    318         mProceedButtonPressed = false;
    319     }
    320 
    321     private class DuplicateCheckTask extends AsyncTask<Void, Void, Account> {
    322 
    323         private final long mAccountId;
    324         private final String mCheckHost;
    325         private final String mCheckLogin;
    326         private final int mCheckSettingsMode;
    327 
    328         public DuplicateCheckTask(long accountId, String checkHost, String checkLogin,
    329                 int checkSettingsMode) {
    330             mAccountId = accountId;
    331             mCheckHost = checkHost;
    332             mCheckLogin = checkLogin;
    333             mCheckSettingsMode = checkSettingsMode;
    334         }
    335 
    336         @Override
    337         protected Account doInBackground(Void... params) {
    338             Account account = Utility.findExistingAccount(mContext, mAccountId,
    339                     mCheckHost, mCheckLogin);
    340             return account;
    341         }
    342 
    343         @Override
    344         protected void onPostExecute(Account duplicateAccount) {
    345             AccountServerBaseFragment fragment = AccountServerBaseFragment.this;
    346             if (duplicateAccount != null) {
    347                 // Show duplicate account warning
    348                 DuplicateAccountDialogFragment dialogFragment =
    349                     DuplicateAccountDialogFragment.newInstance(duplicateAccount.mDisplayName);
    350                 dialogFragment.show(fragment.getFragmentManager(),
    351                         DuplicateAccountDialogFragment.TAG);
    352             } else {
    353                 // Otherwise, proceed with the save/check
    354                 mCallback.onProceedNext(mCheckSettingsMode, fragment);
    355             }
    356             clearButtonBounce();
    357         }
    358     }
    359 
    360     /**
    361      * Implements AccountCheckSettingsFragment.Callbacks
    362      *
    363      * Handle OK or error result from check settings.  Save settings (async), and then
    364      * exit to previous fragment.
    365      */
    366     @Override
    367     public void onCheckSettingsComplete(final int settingsResult) {
    368         new AsyncTask<Void, Void, Void>() {
    369             @Override
    370             protected Void doInBackground(Void... params) {
    371                 if (settingsResult == AccountCheckSettingsFragment.CHECK_SETTINGS_OK) {
    372                     if (SetupData.getFlowMode() == SetupData.FLOW_MODE_EDIT) {
    373                         saveSettingsAfterEdit();
    374                     } else {
    375                         saveSettingsAfterSetup();
    376                     }
    377                 }
    378                 return null;
    379             }
    380 
    381             @Override
    382             protected void onPostExecute(Void result) {
    383                 // Signal to owning activity that a settings check completed
    384                 mCallback.onCheckSettingsComplete(settingsResult, SetupData.getFlowMode());
    385             }
    386         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    387     }
    388 
    389     /**
    390      * Implements AccountCheckSettingsFragment.Callbacks
    391      * This is overridden only by AccountSetupExchange
    392      */
    393     @Override
    394     public void onAutoDiscoverComplete(int result, HostAuth hostAuth) {
    395         throw new IllegalStateException();
    396     }
    397 
    398     /**
    399      * Returns whether or not any settings have changed.
    400      */
    401     public boolean haveSettingsChanged() {
    402         Account account = SetupData.getAccount();
    403 
    404         HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext);
    405         boolean sendChanged = (mLoadedSendAuth != null && !mLoadedSendAuth.equals(sendAuth));
    406 
    407         HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
    408         boolean recvChanged = (mLoadedRecvAuth != null && !mLoadedRecvAuth.equals(recvAuth));
    409 
    410         return sendChanged || recvChanged;
    411     }
    412 
    413     /**
    414      * Save settings after "OK" result from checker.  Concrete classes must implement.
    415      * This is called from a worker thread and is allowed to perform DB operations.
    416      */
    417     public abstract void saveSettingsAfterEdit();
    418 
    419     /**
    420      * Save settings after "OK" result from checker.  Concrete classes must implement.
    421      * This is called from a worker thread and is allowed to perform DB operations.
    422      */
    423     public abstract void saveSettingsAfterSetup();
    424 
    425     /**
    426      * Respond to a click of the "Next" button.  Concrete classes must implement.
    427      */
    428     public abstract void onNext();
    429 }
    430