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