Home | History | Annotate | Download | only in form
      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
     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