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