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 */ onEnableProceedButtons(boolean enable)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 */ onProceedNext(int checkMode, AccountServerBaseFragment target)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 */ onCheckSettingsComplete(int result, int setupMode)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(); onEnableProceedButtons(boolean enable)106 @Override public void onEnableProceedButtons(boolean enable) { } onProceedNext(int checkMode, AccountServerBaseFragment target)107 @Override public void onProceedNext(int checkMode, AccountServerBaseFragment target) { } onCheckSettingsComplete(int result, int setupMode)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 */ getSettingsModeArgs()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 AccountServerBaseFragment()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 onCreate(Bundle savedInstanceState)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 */ onCreateViewSettingsMode(View view)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 onActivityCreated(Bundle savedInstanceState)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 onSaveInstanceState(Bundle outState)170 public void onSaveInstanceState(Bundle outState) { 171 outState.putString(BUNDLE_KEY_ACTIVITY_TITLE, (String) getActivity().getTitle()); 172 } 173 174 @Override onAttach(Activity activity)175 public void onAttach(Activity activity) { 176 super.onAttach(activity); 177 mContext = activity; 178 } 179 180 @Override onDetach()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 onPause()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 onClick(View v)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 */ setCallback(Callback callback)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 */ enableNextButton(boolean enable)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 */ startDuplicateTaskCheck(long accountId, String checkHost, String checkLogin, int checkSettingsMode)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 */ makeTextViewUneditable(final TextView view, final String errorMessage)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 */ clearButtonBounce()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 DuplicateCheckTask(long accountId, String checkHost, String checkLogin, int checkSettingsMode)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 doInBackground(Void... params)337 protected Account doInBackground(Void... params) { 338 Account account = Utility.findExistingAccount(mContext, mAccountId, 339 mCheckHost, mCheckLogin); 340 return account; 341 } 342 343 @Override onPostExecute(Account duplicateAccount)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 onCheckSettingsComplete(final int settingsResult)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 onAutoDiscoverComplete(int result, HostAuth hostAuth)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 */ haveSettingsChanged()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 */ saveSettingsAfterEdit()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 */ saveSettingsAfterSetup()423 public abstract void saveSettingsAfterSetup(); 424 425 /** 426 * Respond to a click of the "Next" button. Concrete classes must implement. 427 */ onNext()428 public abstract void onNext(); 429 } 430