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