AccountServerBaseFragment.java revision 03cd72805dab0379ed255d151f1c17cc60655fc3
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity.setup;
18
19import android.app.Activity;
20import android.app.Fragment;
21import android.content.Context;
22import android.os.AsyncTask;
23import android.os.Bundle;
24import android.view.KeyEvent;
25import android.view.View;
26import android.view.View.OnClickListener;
27import android.view.View.OnFocusChangeListener;
28import android.view.inputmethod.EditorInfo;
29import android.view.inputmethod.InputMethodManager;
30import android.widget.Button;
31import android.widget.TextView;
32import android.widget.TextView.OnEditorActionListener;
33
34import com.android.email.R;
35import com.android.email.activity.UiUtilities;
36import com.android.emailcommon.provider.Account;
37import com.android.emailcommon.provider.HostAuth;
38import com.android.emailcommon.utility.Utility;
39
40import java.net.URI;
41import 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 */
51public 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