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 
17 package com.android.tv.settings.form;
18 
19 import com.android.tv.settings.dialog.old.Action;
20 import com.android.tv.settings.dialog.old.ActionAdapter;
21 import com.android.tv.settings.dialog.old.ActionFragment;
22 import com.android.tv.settings.dialog.old.ContentFragment;
23 import com.android.tv.settings.dialog.old.DialogActivity;
24 import com.android.tv.settings.dialog.old.EditTextFragment;
25 
26 import android.app.Fragment;
27 import android.content.Intent;
28 import android.os.Bundle;
29 import android.util.Log;
30 import android.view.KeyEvent;
31 import android.widget.TextView;
32 
33 import java.util.ArrayList;
34 import 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  */
42 public 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
onActionClicked(Action action)57     public void onActionClicked(Action action) {
58         if (mListener != null) {
59             mListener.onActionClicked(action);
60         }
61     }
62 
63     @Override
onBackPressed()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
onBundlePageResult(FormPage page, Bundle bundleResults)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
onFormComplete()106     public void onFormComplete() {
107         onComplete(mFormPages);
108     }
109 
110     @Override
onFormCancelled()111     public void onFormCancelled() {
112         onCancel(mFormPages);
113     }
114 
115     @Override
onCreate(Bundle savedInstanceState)116     protected void onCreate(Bundle savedInstanceState) {
117         performNextStep();
118         super.onCreate(savedInstanceState);
119     }
120 
121     @Override
onActivityResult(int requestCode, int resultCode, Intent data)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      */
onPageComplete(FormPage formPage)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      */
onComplete(ArrayList<FormPage> formPages)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      */
onCancel(ArrayList<FormPage> formPages)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      */
displayPage(FormPage formPage, FormPageResultListener listener, boolean forward)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      */
displayFormResults(ArrayList<FormPage> formPages, FormResultListener listener)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      */
getMainTitle()218     protected String getMainTitle() {
219         return "";
220     }
221 
222     /**
223      * @return the action title to indicate the form is correct.
224      */
getFormIsCorrectActionTitle()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      */
getFormCancelActionTitle()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      */
getContentFragment(FormPage formPage)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      */
getResultContentFragment()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      */
getPasswordEditTextFragment(String initialText)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      */
getEditTextFragment(String initialText)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      */
getActionFragment(FormPage formPage, ArrayList<Action> actions, Action selectedAction)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      */
getResultActionFragment(ArrayList<Action> actions)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      */
addPage(FormPage formPage)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      */
removePage(FormPage formPage)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      */
clear()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      */
clearAfter(FormPage formPage)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      */
undisplayCurrentPage()368     protected void undisplayCurrentPage() {
369     }
370 
performNextStep()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 
findNextIncompleteStep()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 
displayCurrentStep(boolean forward)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 
createResultContentFragment()421     private Fragment createResultContentFragment() {
422         return getResultContentFragment();
423     }
424 
createResultActionFragment(final ArrayList<FormPage> formPages, final FormResultListener listener)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 
getKeyFromKey(String key)461     private Key getKeyFromKey(String key) {
462         try {
463             return Key.valueOf(key);
464         } catch (IllegalArgumentException iae) {
465             return null;
466         }
467     }
468 
getActions(FormPage formPage)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 
getResultActions()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 
createActionFragment(final FormPage formPage)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 
createPasswordEditTextFragment(final FormPage formPage)515     private Fragment createPasswordEditTextFragment(final FormPage formPage) {
516         EditTextFragment editTextFragment = getPasswordEditTextFragment(formPage.getDataSummary());
517         attachListeners(editTextFragment, formPage);
518         return editTextFragment;
519     }
520 
createEditTextFragment(final FormPage formPage)521     private Fragment createEditTextFragment(final FormPage formPage) {
522         EditTextFragment editTextFragment = getEditTextFragment(formPage.getDataSummary());
523         attachListeners(editTextFragment, formPage);
524         return editTextFragment;
525     }
526 
attachListeners(EditTextFragment editTextFragment, final FormPage formPage)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 
handleStringPageResult(FormPage page, String stringResults)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