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