1/*
2 * Copyright (C) 2014 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.tv.settings.form;
18
19import com.android.tv.settings.dialog.old.Action;
20import com.android.tv.settings.dialog.old.ActionAdapter;
21import com.android.tv.settings.dialog.old.ActionFragment;
22import com.android.tv.settings.dialog.old.ContentFragment;
23import com.android.tv.settings.dialog.old.DialogActivity;
24import com.android.tv.settings.dialog.old.EditTextFragment;
25import com.android.tv.settings.R;
26
27import android.app.Fragment;
28import android.content.Intent;
29import android.os.Bundle;
30import android.util.Log;
31import android.view.KeyEvent;
32import android.widget.TextView;
33
34import java.util.ArrayList;
35import java.util.Stack;
36
37/**
38 * Implements a MultiPagedForm.
39 * <p>
40 * This is a multi-paged form that can be used for fragment transitions used in
41 * such as setup, add network, and add credit cards
42 */
43public abstract class MultiPagedForm extends DialogActivity implements ActionAdapter.Listener,
44    FormPageResultListener, FormResultListener {
45
46    private static final int INTENT_FORM_PAGE_DATA_REQUEST = 1;
47    private static final String TAG = "MultiPagedForm";
48
49    private enum Key {
50        DONE, CANCEL
51    }
52
53    protected final ArrayList<FormPage> mFormPages = new ArrayList<FormPage>();
54    private final Stack<Object> mFlowStack = new Stack<Object>();
55    private ActionAdapter.Listener mListener = null;
56
57    @Override
58    public void onActionClicked(Action action) {
59        if (mListener != null) {
60            mListener.onActionClicked(action);
61        }
62    }
63
64    @Override
65    public void onBackPressed() {
66
67        // If we don't have a page to go back to, finish as cancelled.
68        if (mFlowStack.size() < 1) {
69            setResult(RESULT_CANCELED);
70            finish();
71            return;
72        }
73
74        // Pop the current location off the stack.
75        mFlowStack.pop();
76
77        // Peek at the previous location on the stack.
78        Object lastLocation = mFlowStack.isEmpty() ? null : mFlowStack.peek();
79
80        if (lastLocation instanceof FormPage && !mFormPages.contains(lastLocation)) {
81            onBackPressed();
82        } else {
83            displayCurrentStep(false);
84            if (mFlowStack.isEmpty()) {
85                setResult(RESULT_CANCELED);
86                finish();
87            }
88        }
89    }
90
91    @Override
92    public void onBundlePageResult(FormPage page, Bundle bundleResults) {
93        // Complete the form with the results.
94        page.complete(bundleResults);
95
96        // Indicate that we've completed a page. If we get back false it means
97        // the data was invalid and the page must be filled out again.
98        // Otherwise, we move on to the next page.
99        if (!onPageComplete(page)) {
100            displayCurrentStep(false);
101        } else {
102            performNextStep();
103        }
104    }
105
106    @Override
107    public void onFormComplete() {
108        onComplete(mFormPages);
109    }
110
111    @Override
112    public void onFormCancelled() {
113        onCancel(mFormPages);
114    }
115
116    @Override
117    protected void onCreate(Bundle savedInstanceState) {
118        performNextStep();
119        super.onCreate(savedInstanceState);
120    }
121
122    @Override
123    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
124        if (requestCode == INTENT_FORM_PAGE_DATA_REQUEST) {
125            if (resultCode == RESULT_OK) {
126                overridePendingTransition(
127                        R.anim.wps_activity_open_in, R.anim.wps_activity_open_out);
128                Object currentLocation = mFlowStack.peek();
129                if (currentLocation instanceof FormPage) {
130                    FormPage page = (FormPage) currentLocation;
131                    Bundle results = data == null ? null : data.getExtras();
132                    if (data == null) {
133                        Log.w(TAG, "Intent result was null!");
134                    } else if (results == null) {
135                        Log.w(TAG, "Intent result extras were null!");
136                    } else if (!results.containsKey(FormPage.DATA_KEY_SUMMARY_STRING)) {
137                        Log.w(TAG, "Intent result extras didn't have the result summary key!");
138                    }
139                    onBundlePageResult(page, results);
140                } else {
141                    Log.e(TAG, "Our current location wasn't on the top of the stack!");
142                }
143            } else {
144                overridePendingTransition(
145                        R.anim.wps_activity_close_in, R.anim.wps_activity_close_out);
146                onBackPressed();
147            }
148        }
149    }
150
151    /**
152     * Called when a form page completes. If necessary, add or remove any pages
153     * from the form before this call completes. If all pages are complete when
154     * onPageComplete returns, the form will be considered finished and the form
155     * results will be displayed for confirmation.
156     *
157     * @param formPage the page that was completed.
158     * @return true if the form can continue to the next incomplete page, or
159     *         false if the data input is invalid and the form page must be
160     *         completed again.
161     */
162    protected abstract boolean onPageComplete(FormPage formPage);
163
164    /**
165     * Called when all form pages have been completed and the user has accepted
166     * them.
167     *
168     * @param formPages the pages that were completed. Any pages removed during
169     *            the completion of the form are not included.
170     */
171    protected abstract void onComplete(ArrayList<FormPage> formPages);
172
173    /**
174     * Called when all form pages have been completed but the user wants to
175     * cancel the form and discard the results.
176     *
177     * @param formPages the pages that were completed. Any pages removed during
178     *            the completion of the form are not included.
179     */
180    protected abstract void onCancel(ArrayList<FormPage> formPages);
181
182    /**
183     * Override this to fully customize the display of the page.
184     *
185     * @param formPage the page that should be displayed.
186     * @param listener the listener to notify when the page is complete.
187     */
188    protected void displayPage(FormPage formPage, FormPageResultListener listener,
189            boolean forward) {
190        switch (formPage.getType()) {
191            case PASSWORD_INPUT:
192                setContentAndActionFragments(getContentFragment(formPage),
193                        createPasswordEditTextFragment(formPage));
194                break;
195            case TEXT_INPUT:
196                setContentAndActionFragments(getContentFragment(formPage),
197                        createEditTextFragment(formPage));
198                break;
199            case MULTIPLE_CHOICE:
200                setContentAndActionFragments(getContentFragment(formPage),
201                        createActionFragment(formPage));
202                break;
203            case INTENT:
204            default:
205                break;
206        }
207    }
208
209    /**
210     * Override this to fully customize the display of the form results.
211     *
212     * @param formPages the pages that were whose results should be displayed.
213     * @param listener the listener to notify when the form is complete or has been cancelled.
214     */
215    protected void displayFormResults(ArrayList<FormPage> formPages, FormResultListener listener) {
216        setContentAndActionFragments(createResultContentFragment(),
217                createResultActionFragment(formPages, listener));
218    }
219
220    /**
221     * @return the main title for this multipage form.
222     */
223    protected String getMainTitle() {
224        return "";
225    }
226
227    /**
228     * @return the action title to indicate the form is correct.
229     */
230    protected String getFormIsCorrectActionTitle() {
231        return "";
232    }
233
234    /**
235     * @return the action title to indicate the form should be canceled and its
236     *         results discarded.
237     */
238    protected String getFormCancelActionTitle() {
239        return "";
240    }
241
242    /**
243     * Override this to provide a custom Fragment for displaying the content
244     * portion of the page.
245     *
246     * @param formPage the page the Fragment should display.
247     * @return a Fragment for identifying the current step.
248     */
249    protected Fragment getContentFragment(FormPage formPage) {
250        return ContentFragment.newInstance(formPage.getTitle());
251    }
252
253    /**
254     * Override this to provide a custom Fragment for displaying the content
255     * portion of the form results.
256     *
257     * @return a Fragment for giving context to the result page.
258     */
259    protected Fragment getResultContentFragment() {
260        return ContentFragment.newInstance(getMainTitle());
261    }
262
263    /**
264     * Override this to provide a custom EditTextFragment for displaying a form
265     * page for password input. Warning: the OnEditorActionListener of this
266     * fragment will be overridden.
267     *
268     * @param initialText initial text that should be displayed in the edit
269     *            field.
270     * @return an EditTextFragment for password input.
271     */
272    protected EditTextFragment getPasswordEditTextFragment(String initialText) {
273        return EditTextFragment.newInstance(null, initialText, true /* password */);
274    }
275
276    /**
277     * Override this to provide a custom EditTextFragment for displaying a form
278     * page for text input. Warning: the OnEditorActionListener of this fragment
279     * will be overridden.
280     *
281     * @param initialText initial text that should be displayed in the edit
282     *            field.
283     * @return an EditTextFragment for custom input.
284     */
285    protected EditTextFragment getEditTextFragment(String initialText) {
286        return EditTextFragment.newInstance(null, initialText);
287    }
288
289    /**
290     * Override this to provide a custom ActionFragment for displaying a form
291     * page for a list of choices.
292     *
293     * @param formPage the page the ActionFragment is for.
294     * @param actions the actions the ActionFragment should display.
295     * @param selectedAction the action in actions that is currently selected,
296     *            or null if none are selected.
297     * @return an ActionFragment displaying the given actions.
298     */
299    protected ActionFragment getActionFragment(FormPage formPage, ArrayList<Action> actions,
300            Action selectedAction) {
301        ActionFragment actionFragment = ActionFragment.newInstance(actions);
302        if (selectedAction != null) {
303            int indexOfSelection = actions.indexOf(selectedAction);
304            if (indexOfSelection >= 0) {
305                // TODO: Set initial focus action:
306                // actionFragment.setSelection(indexOfSelection);
307            }
308        }
309        return actionFragment;
310    }
311
312    /**
313     * Override this to provide a custom ActionFragment for displaying the list
314     * of page results.
315     *
316     * @param actions the actions the ActionFragment should display.
317     * @return an ActionFragment displaying the given form results.
318     */
319    protected ActionFragment getResultActionFragment(ArrayList<Action> actions) {
320        return ActionFragment.newInstance(actions);
321    }
322
323    /**
324     * Adds the page to the end of the form. Only call this before onCreate or
325     * during onPageComplete.
326     *
327     * @param formPage the page to add to the end of the form.
328     */
329    protected void addPage(FormPage formPage) {
330        mFormPages.add(formPage);
331    }
332
333    /**
334     * Removes the page from the form. Only call this before onCreate or during
335     * onPageComplete.
336     *
337     * @param formPage the page to remove from the form.
338     */
339    protected void removePage(FormPage formPage) {
340        mFormPages.remove(formPage);
341    }
342
343    /**
344     * Clears all pages from the form. Only call this before onCreate or during
345     * onPageComplete.
346     */
347    protected void clear() {
348        mFormPages.clear();
349    }
350
351    /**
352     * Clears all pages after the given page from the form. Only call this
353     * before onCreate or during onPageComplete.
354     *
355     * @param formPage all pages after this page in the form will be removed
356     *            from the form.
357     */
358    protected void clearAfter(FormPage formPage) {
359        int indexOfPage = mFormPages.indexOf(formPage);
360        if (indexOfPage >= 0) {
361            for (int i = mFormPages.size() - 1; i > indexOfPage; i--) {
362                mFormPages.remove(i);
363            }
364        }
365    }
366
367    /**
368     * Stop display the currently displayed page. Note that this does <b>not</b>
369     * remove the form page from the set of form pages for this form, it is just
370     * no longer displayed and no replacement is provided, the screen should be
371     * empty after this method.
372     */
373    protected void undisplayCurrentPage() {
374    }
375
376    private void performNextStep() {
377
378        // First see if there are any incomplete form pages.
379        FormPage nextIncompleteStep = findNextIncompleteStep();
380
381        // If all the pages we have are complete, display the results.
382        if (nextIncompleteStep == null) {
383            mFlowStack.push(this);
384        } else {
385            mFlowStack.push(nextIncompleteStep);
386        }
387        displayCurrentStep(true);
388    }
389
390    private FormPage findNextIncompleteStep() {
391        for (int i = 0, size = mFormPages.size(); i < size; i++) {
392            FormPage formPage = mFormPages.get(i);
393            if (!formPage.isComplete()) {
394                return formPage;
395            }
396        }
397        return null;
398    }
399
400    private void displayCurrentStep(boolean forward) {
401
402        if (!mFlowStack.isEmpty()) {
403            Object currentLocation = mFlowStack.peek();
404
405            if (currentLocation instanceof MultiPagedForm) {
406                displayFormResults(mFormPages, this);
407            } else if (currentLocation instanceof FormPage) {
408                FormPage page = (FormPage) currentLocation;
409                if (page.getType() == FormPage.Type.INTENT) {
410                    startActivityForResult(page.getIntent(), INTENT_FORM_PAGE_DATA_REQUEST);
411                }
412                displayPage(page, this, forward);
413            } else {
414                // If this is an unexpected type, something went wrong, finish as
415                // cancelled.
416                setResult(RESULT_CANCELED);
417                finish();
418            }
419        } else {
420            undisplayCurrentPage();
421        }
422
423    }
424
425    private Fragment createResultContentFragment() {
426        return getResultContentFragment();
427    }
428
429    private Fragment createResultActionFragment(final ArrayList<FormPage> formPages,
430            final FormResultListener listener) {
431
432        mListener = new ActionAdapter.Listener() {
433
434            @Override
435            public void onActionClicked(Action action) {
436                Key key = getKeyFromKey(action.getKey());
437                if (key != null) {
438                    switch (key) {
439                        case DONE:
440                            listener.onFormComplete();
441                            break;
442                        case CANCEL:
443                            listener.onFormCancelled();
444                            break;
445                        default:
446                            break;
447                    }
448                } else {
449                    String formPageKey = action.getKey();
450                    for (int i = 0, size = formPages.size(); i < size; i++) {
451                        FormPage formPage = formPages.get(i);
452                        if (formPageKey.equals(formPage.getTitle())) {
453                            mFlowStack.push(formPage);
454                            displayCurrentStep(true);
455                            break;
456                        }
457                    }
458                }
459            }
460        };
461
462        return getResultActionFragment(getResultActions());
463    }
464
465    private Key getKeyFromKey(String key) {
466        try {
467            return Key.valueOf(key);
468        } catch (IllegalArgumentException iae) {
469            return null;
470        }
471    }
472
473    private ArrayList<Action> getActions(FormPage formPage) {
474        ArrayList<Action> actions = new ArrayList<Action>();
475        for (String choice : formPage.getChoices()) {
476            actions.add(new Action.Builder().key(choice).title(choice).build());
477        }
478        return actions;
479    }
480
481    private ArrayList<Action> getResultActions() {
482        ArrayList<Action> actions = new ArrayList<Action>();
483        for (int i = 0, size = mFormPages.size(); i < size; i++) {
484            FormPage formPage = mFormPages.get(i);
485            actions.add(new Action.Builder().key(formPage.getTitle())
486                    .title(formPage.getDataSummary()).description(formPage.getTitle()).build());
487        }
488        actions.add(new Action.Builder().key(Key.CANCEL.name())
489                .title(getFormCancelActionTitle()).build());
490        actions.add(new Action.Builder().key(Key.DONE.name())
491                .title(getFormIsCorrectActionTitle()).build());
492        return actions;
493    }
494
495    private Fragment createActionFragment(final FormPage formPage) {
496        mListener = new ActionAdapter.Listener() {
497
498            @Override
499            public void onActionClicked(Action action) {
500                handleStringPageResult(formPage, action.getKey());
501            }
502        };
503
504        ArrayList<Action> actions = getActions(formPage);
505
506        Action selectedAction = null;
507        String choice = formPage.getDataSummary();
508        for (int i = 0, size = actions.size(); i < size; i++) {
509            Action action = actions.get(i);
510            if (action.getKey().equals(choice)) {
511                selectedAction = action;
512                break;
513            }
514        }
515
516        return getActionFragment(formPage, actions, selectedAction);
517    }
518
519    private Fragment createPasswordEditTextFragment(final FormPage formPage) {
520        EditTextFragment editTextFragment = getPasswordEditTextFragment(formPage.getDataSummary());
521        attachListeners(editTextFragment, formPage);
522        return editTextFragment;
523    }
524
525    private Fragment createEditTextFragment(final FormPage formPage) {
526        EditTextFragment editTextFragment = getEditTextFragment(formPage.getDataSummary());
527        attachListeners(editTextFragment, formPage);
528        return editTextFragment;
529    }
530
531    private void attachListeners(EditTextFragment editTextFragment, final FormPage formPage) {
532
533        editTextFragment.setOnEditorActionListener(new TextView.OnEditorActionListener() {
534
535            @Override
536            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
537                handleStringPageResult(formPage, v.getText().toString());
538                return true;
539            }
540        });
541    }
542
543    private void handleStringPageResult(FormPage page, String stringResults) {
544        Bundle bundleResults = new Bundle();
545        bundleResults.putString(FormPage.DATA_KEY_SUMMARY_STRING, stringResults);
546        onBundlePageResult(page, bundleResults);
547    }
548}
549