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.example.android.leanback; 18 19import android.app.Activity; 20import android.app.Fragment; 21import android.app.FragmentManager; 22import android.content.Context; 23import android.content.res.Configuration; 24import android.graphics.drawable.Drawable; 25import android.os.Bundle; 26import android.support.v17.leanback.app.GuidedStepFragment; 27import android.support.v17.leanback.widget.GuidanceStylist; 28import android.support.v17.leanback.widget.GuidanceStylist.Guidance; 29import android.support.v17.leanback.widget.GuidedAction; 30import android.support.v17.leanback.widget.GuidedActionsStylist; 31import android.support.v17.leanback.widget.GuidedDatePickerAction; 32import android.support.v4.content.res.ResourcesCompat; 33import android.text.InputType; 34import android.text.TextUtils; 35import android.util.Log; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.ViewGroup; 39import android.view.inputmethod.EditorInfo; 40 41import java.util.ArrayList; 42import java.util.Calendar; 43import java.util.List; 44 45/** 46 * Activity that showcases different aspects of GuidedStepFragments. 47 */ 48public class GuidedStepActivity extends Activity { 49 50 private static final int BACK = 2; 51 52 private static final int FIRST_NAME = 3; 53 private static final int LAST_NAME = 4; 54 private static final int PASSWORD = 5; 55 private static final int PAYMENT = 6; 56 private static final int NEW_PAYMENT = 7; 57 private static final int PAYMENT_EXPIRE = 8; 58 59 private static final long RADIO_ID_BASE = 0; 60 private static final long CHECKBOX_ID_BASE = 100; 61 62 private static final long DEFAULT_OPTION = RADIO_ID_BASE; 63 64 private static final String[] OPTION_NAMES = { "Option A", "Option B", "Option C" }; 65 private static final String[] OPTION_DESCRIPTIONS = { "Here's one thing you can do", 66 "Here's another thing you can do", "Here's one more thing you can do" }; 67 68 private static final String TAG = GuidedStepActivity.class.getSimpleName(); 69 70 @Override 71 protected void onCreate(Bundle savedInstanceState) { 72 Log.v(TAG, "onCreate"); 73 super.onCreate(savedInstanceState); 74 setContentView(R.layout.guided_step_activity); 75 if (savedInstanceState == null) { 76 GuidedStepFragment.addAsRoot(this, new FirstStepFragment(), R.id.lb_guidedstep_host); 77 } 78 } 79 80 @Override 81 public void onConfigurationChanged(Configuration newConfig) { 82 Log.v(TAG, "onConfigurationChanged"); 83 super.onConfigurationChanged(newConfig); 84 } 85 86 @Override 87 protected void onSaveInstanceState(Bundle outState) { 88 Log.v(TAG, "onSaveInstanceState"); 89 super.onSaveInstanceState(outState); 90 } 91 92 @Override 93 protected void onRestoreInstanceState(Bundle savedInstanceState) { 94 Log.v(TAG, "onRestoreInstanceState"); 95 super.onRestoreInstanceState(savedInstanceState); 96 } 97 98 private static GuidedAction addAction(List<GuidedAction> actions, long id, String title, 99 String desc) { 100 GuidedAction action; 101 actions.add(action = new GuidedAction.Builder(null) 102 .id(id) 103 .title(title) 104 .description(desc) 105 .build()); 106 return action; 107 } 108 109 private static GuidedAction addAction(List<GuidedAction> actions, long id, String title, 110 String desc, List<GuidedAction> subActions) { 111 GuidedAction action; 112 actions.add(action = new GuidedAction.Builder(null) 113 .id(id) 114 .title(title) 115 .description(desc) 116 .subActions(subActions) 117 .build()); 118 return action; 119 } 120 121 private static GuidedAction addEditableAction(Context context, List<GuidedAction> actions, 122 long id, String title, String desc) { 123 GuidedAction action; 124 actions.add(action = new GuidedAction.Builder(context) 125 .id(id) 126 .title(title) 127 .description(desc) 128 .editable(true) 129 .icon(R.drawable.lb_ic_search_mic) 130 .build()); 131 return action; 132 } 133 134 private static GuidedAction addEditableAction(List<GuidedAction> actions, long id, String title, 135 String editTitle, String desc) { 136 GuidedAction action; 137 actions.add(action = new GuidedAction.Builder(null) 138 .id(id) 139 .title(title) 140 .editTitle(editTitle) 141 .description(desc) 142 .editable(true) 143 .build()); 144 return action; 145 } 146 147 private static GuidedAction addEditableAction(List<GuidedAction> actions, long id, String title, 148 String editTitle, int editInputType, String desc, String editDesc) { 149 GuidedAction action; 150 actions.add(action = new GuidedAction.Builder(null) 151 .id(id) 152 .title(title) 153 .editTitle(editTitle) 154 .editInputType(editInputType) 155 .description(desc) 156 .editDescription(editDesc) 157 .editable(true) 158 .build()); 159 return action; 160 } 161 162 private static GuidedDatePickerAction addDatePickerAction(List<GuidedAction> actions, long id, 163 String title) { 164 GuidedDatePickerAction action; 165 actions.add(action = new GuidedDatePickerAction.Builder(null) 166 .id(id) 167 .title(title) 168 .datePickerFormat("MY") 169 .build()); 170 return action; 171 } 172 173 private static GuidedAction addEditableDescriptionAction(List<GuidedAction> actions, long id, 174 String title, String desc, String editDescription, int descriptionEditInputType) { 175 GuidedAction action; 176 actions.add(action = new GuidedAction.Builder(null) 177 .id(id) 178 .title(title) 179 .description(desc) 180 .editDescription(editDescription) 181 .descriptionEditInputType(descriptionEditInputType) 182 .descriptionEditable(true) 183 .build()); 184 return action; 185 } 186 187 private static GuidedAction addCheckedAction(List<GuidedAction> actions, long id, 188 String title, String desc, int checkSetId) { 189 GuidedAction action; 190 actions.add(action = new GuidedAction.Builder(null) 191 .id(id) 192 .title(title) 193 .description(desc) 194 .checkSetId(checkSetId) 195 .build()); 196 return action; 197 } 198 199 public static class FirstStepFragment extends GuidedStepFragment { 200 201 @Override 202 public int onProvideTheme() { 203 return R.style.Theme_Example_Leanback_GuidedStep_First; 204 } 205 206 @Override 207 public Guidance onCreateGuidance(Bundle savedInstanceState) { 208 String title = getString(R.string.guidedstep_first_title); 209 String breadcrumb = getString(R.string.guidedstep_first_breadcrumb); 210 String description = getString(R.string.guidedstep_first_description); 211 final Context context = getActivity(); 212 Drawable icon = ResourcesCompat.getDrawable(context.getResources(), 213 R.drawable.ic_main_icon, context.getTheme()); 214 return new Guidance(title, description, breadcrumb, icon); 215 } 216 217 @Override 218 public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { 219 Context context = getActivity(); 220 actions.add(new GuidedAction.Builder(context) 221 .clickAction(GuidedAction.ACTION_ID_CONTINUE) 222 .description("Let's do it") 223 .build()); 224 actions.add(new GuidedAction.Builder(context) 225 .clickAction(GuidedAction.ACTION_ID_CANCEL) 226 .description("Never mind") 227 .build()); 228 } 229 230 @Override 231 public void onGuidedActionClicked(GuidedAction action) { 232 FragmentManager fm = getFragmentManager(); 233 if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) { 234 GuidedStepFragment.add(fm, new SecondStepFragment(), R.id.lb_guidedstep_host); 235 } else if (action.getId() == GuidedAction.ACTION_ID_CANCEL){ 236 finishGuidedStepFragments(); 237 } 238 } 239 } 240 241 public interface NewPaymentFragmentTarget { 242 void onNewPaymentFragmentStarted(); 243 void onNewPaymentAdded(int selection); 244 } 245 246 static ArrayList<String> sCards = new ArrayList<String>(); 247 static int sSelectedCard = -1; 248 static { 249 sCards.add("Visa-1234"); 250 sCards.add("Master-4321"); 251 } 252 253 public static class NewPaymentStepFragment extends GuidedStepFragment { 254 255 NewPaymentFragmentTarget mNewPaymentTarget; 256 257 @Override 258 public void onCreate(Bundle savedInstance) { 259 super.onCreate(savedInstance); 260 Fragment targetFragment = getTargetFragment(); 261 if (targetFragment instanceof NewPaymentFragmentTarget) { 262 mNewPaymentTarget = ((NewPaymentFragmentTarget) targetFragment); 263 mNewPaymentTarget.onNewPaymentFragmentStarted(); 264 } 265 } 266 267 @Override 268 public Guidance onCreateGuidance(Bundle savedInstanceState) { 269 String title = getString(R.string.guidedstep_newpayment_title); 270 String breadcrumb = getString(R.string.guidedstep_newpayment_breadcrumb); 271 String description = getString(R.string.guidedstep_newpayment_description); 272 final Context context = getActivity(); 273 Drawable icon = ResourcesCompat.getDrawable(context.getResources(), 274 R.drawable.ic_main_icon, context.getTheme()); 275 return new Guidance(title, description, breadcrumb, icon); 276 } 277 278 @Override 279 public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { 280 addEditableAction(actions, NEW_PAYMENT, "Input credit card number", "", 281 InputType.TYPE_CLASS_NUMBER, 282 "Input credit card number", "Input credit card number"); 283 addDatePickerAction(actions, PAYMENT_EXPIRE, "Exp:"); 284 } 285 286 @Override 287 public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { 288 Context context = getActivity(); 289 actions.add(new GuidedAction.Builder(context).clickAction(GuidedAction.ACTION_ID_OK) 290 .build()); 291 actions.get(actions.size() - 1).setEnabled(false); 292 } 293 294 @Override 295 public void onGuidedActionClicked(GuidedAction action) { 296 if (action.getId() == GuidedAction.ACTION_ID_OK) { 297 CharSequence desc = findActionById(NEW_PAYMENT).getDescription(); 298 String cardNumber = desc.subSequence(desc.length() - 4, desc.length()).toString(); 299 String card; 300 if ((Integer.parseInt(cardNumber) & 1) == 0) { 301 card = "Visa "+cardNumber; 302 } else { 303 card = "Master "+cardNumber; 304 } 305 int selection = sCards.size(); 306 sCards.add(card); 307 if (mNewPaymentTarget != null) { 308 mNewPaymentTarget.onNewPaymentAdded(selection); 309 } 310 popBackStackToGuidedStepFragment(NewPaymentStepFragment.class, 311 FragmentManager.POP_BACK_STACK_INCLUSIVE); 312 } 313 } 314 315 @Override 316 public long onGuidedActionEditedAndProceed(GuidedAction action) { 317 if (action.getId() == NEW_PAYMENT) { 318 CharSequence editTitle = action.getEditTitle(); 319 if (isCardNumberValid(editTitle)) { 320 editTitle = editTitle.subSequence(editTitle.length() - 4, editTitle.length()); 321 action.setDescription("Visa XXXX-XXXX-XXXX-" + editTitle); 322 updateOkButton(isExpDateValid(findActionById(PAYMENT_EXPIRE))); 323 return GuidedAction.ACTION_ID_NEXT; 324 } else if (editTitle.length() == 0) { 325 action.setDescription("Input credit card number"); 326 updateOkButton(false); 327 return GuidedAction.ACTION_ID_CURRENT; 328 } else { 329 action.setDescription("Error credit card number"); 330 updateOkButton(false); 331 return GuidedAction.ACTION_ID_CURRENT; 332 } 333 } else if (action.getId() == PAYMENT_EXPIRE) { 334 updateOkButton(isExpDateValid(action) && 335 isCardNumberValid(findActionById(NEW_PAYMENT).getEditTitle())); 336 } 337 return GuidedAction.ACTION_ID_NEXT; 338 } 339 340 boolean isCardNumberValid(CharSequence number) { 341 return TextUtils.isDigitsOnly(number) && number.length() == 16; 342 } 343 344 boolean isExpDateValid(GuidedAction action) { 345 long date = ((GuidedDatePickerAction) action).getDate(); 346 Calendar c = Calendar.getInstance(); 347 c.setTimeInMillis(date); 348 return Calendar.getInstance().before(c); 349 } 350 351 void updateOkButton(boolean enabled) { 352 findButtonActionById(GuidedAction.ACTION_ID_OK).setEnabled(enabled); 353 notifyButtonActionChanged(findButtonActionPositionById(GuidedAction.ACTION_ID_OK)); 354 } 355 } 356 357 public static class SecondStepFragment extends GuidedStepFragment 358 implements NewPaymentFragmentTarget { 359 360 361 boolean mExpandPaymentListInOnCreateView; 362 363 @Override 364 public void onNewPaymentAdded(int selection) { 365 // if a new payment is added, we dont need expand the sub actions list. 366 mExpandPaymentListInOnCreateView = false; 367 sSelectedCard = selection; 368 updatePaymentAction(findActionById(PAYMENT)); 369 findButtonActionById(GuidedAction.ACTION_ID_CONTINUE).setEnabled(sSelectedCard != -1); 370 } 371 372 @Override 373 public void onNewPaymentFragmentStarted() { 374 // if a new payment fragment is opened, when come back we should expand the payment 375 // sub actions list unless user created a new payment in onNewPaymentAdded 376 mExpandPaymentListInOnCreateView = true; 377 } 378 379 @Override 380 public GuidedActionsStylist onCreateActionsStylist() { 381 return new GuidedActionsStylist() { 382 @Override 383 protected void setupImeOptions(GuidedActionsStylist.ViewHolder vh, 384 GuidedAction action) { 385 if (action.getId() == PASSWORD) { 386 vh.getEditableDescriptionView().setImeActionLabel("Confirm!", 387 EditorInfo.IME_ACTION_DONE); 388 } else { 389 super.setupImeOptions(vh, action); 390 } 391 } 392 }; 393 } 394 395 @Override 396 public Guidance onCreateGuidance(Bundle savedInstanceState) { 397 String title = getString(R.string.guidedstep_second_title); 398 String breadcrumb = getString(R.string.guidedstep_second_breadcrumb); 399 String description = getString(R.string.guidedstep_second_description); 400 final Context context = getActivity(); 401 Drawable icon = ResourcesCompat.getDrawable(context.getResources(), 402 R.drawable.ic_main_icon, context.getTheme()); 403 return new Guidance(title, description, breadcrumb, icon); 404 } 405 406 @Override 407 public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { 408 addEditableAction(getActivity(), actions, FIRST_NAME, "Pat", "Your first name"); 409 addEditableAction(getActivity(), actions, LAST_NAME, "Smith", "Your last name"); 410 List<GuidedAction> subActions = new ArrayList<GuidedAction>(); 411 updatePaymentAction(addAction(actions, PAYMENT, "Select Payment", "", subActions)); 412 addEditableDescriptionAction(actions, PASSWORD, "Password", "", "", 413 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); 414 } 415 416 @Override 417 public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { 418 actions.add(new GuidedAction.Builder(getActivity()) 419 .clickAction(GuidedAction.ACTION_ID_CONTINUE) 420 .description("Continue") 421 .enabled(isPasswordValid() && isPaymentValid()) 422 .build()); 423 } 424 425 @Override 426 public void onGuidedActionClicked(GuidedAction action) { 427 if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) { 428 FragmentManager fm = getFragmentManager(); 429 GuidedStepFragment.add(fm, new ThirdStepFragment(), R.id.lb_guidedstep_host); 430 } 431 } 432 433 void updatePaymentAction(GuidedAction paymentAction) { 434 List<GuidedAction> subActions = paymentAction.getSubActions(); 435 subActions.clear(); 436 for (int i = 0; i < sCards.size(); i++) { 437 addCheckedAction(subActions, -1, sCards.get(i), "", 438 GuidedAction.DEFAULT_CHECK_SET_ID); 439 if (i == sSelectedCard) { 440 subActions.get(i).setChecked(true); 441 } 442 } 443 addAction(subActions, NEW_PAYMENT, "Add New Card", ""); 444 paymentAction.setDescription(sSelectedCard == -1 ? "" : sCards.get(sSelectedCard)); 445 } 446 447 @Override 448 public long onGuidedActionEditedAndProceed(GuidedAction action) { 449 if (action.getId() == PASSWORD) { 450 CharSequence password = action.getEditDescription(); 451 if (password.length() > 0) { 452 if (isPaymentValid()) { 453 updateContinue(true); 454 return GuidedAction.ACTION_ID_NEXT; 455 } else { 456 updateContinue(false); 457 return GuidedAction.ACTION_ID_CURRENT; 458 } 459 } else { 460 updateContinue(false); 461 return GuidedAction.ACTION_ID_CURRENT; 462 } 463 } 464 return GuidedAction.ACTION_ID_NEXT; 465 } 466 467 @Override 468 public boolean onSubGuidedActionClicked(GuidedAction action) { 469 if (action.isChecked()) { 470 String payment = action.getTitle().toString(); 471 for (int i = 0; i < sCards.size(); i++) { 472 if (payment.equals(sCards.get(i))) { 473 sSelectedCard = i; 474 findActionById(PAYMENT).setDescription(payment); 475 notifyActionChanged(findActionPositionById(PAYMENT)); 476 updateContinue(isPasswordValid()); 477 break; 478 } 479 } 480 return true; 481 } else { 482 FragmentManager fm = getFragmentManager(); 483 NewPaymentStepFragment newPaymentFragment = new NewPaymentStepFragment(); 484 newPaymentFragment.setTargetFragment(this, 0); 485 GuidedStepFragment.add(fm, newPaymentFragment, R.id.lb_guidedstep_host); 486 return false; 487 } 488 } 489 490 @Override 491 public View onCreateView(LayoutInflater inflater, ViewGroup container, 492 Bundle savedInstanceState) { 493 View view = super.onCreateView(inflater, container, savedInstanceState); 494 if (mExpandPaymentListInOnCreateView) { 495 expandAction(findActionById(PAYMENT), false); 496 } 497 return view; 498 } 499 500 boolean isPaymentValid() { 501 CharSequence paymentType = findActionById(PAYMENT).getDescription(); 502 return (paymentType.length() >= 4 && 503 paymentType.subSequence(0, 4).toString().equals("Visa")) || 504 (paymentType.length() >= 6 && 505 paymentType.subSequence(0, 6).toString().equals("Master")); 506 } 507 508 boolean isPasswordValid() { 509 return findActionById(PASSWORD).getEditDescription().length() > 0; 510 } 511 512 void updateContinue(boolean enabled) { 513 findButtonActionById(GuidedAction.ACTION_ID_CONTINUE).setEnabled(enabled); 514 notifyButtonActionChanged(findButtonActionPositionById( 515 GuidedAction.ACTION_ID_CONTINUE)); 516 } 517 } 518 519 public static class ThirdStepFragment extends GuidedStepFragment { 520 521 private long mSelectedOption = DEFAULT_OPTION; 522 523 @Override 524 public Guidance onCreateGuidance(Bundle savedInstanceState) { 525 String title = getString(R.string.guidedstep_third_title); 526 String breadcrumb = getString(R.string.guidedstep_third_breadcrumb); 527 String description = getString(R.string.guidedstep_third_description); 528 final Context context = getActivity(); 529 Drawable icon = ResourcesCompat.getDrawable(context.getResources(), 530 R.drawable.ic_main_icon, context.getTheme()); 531 return new Guidance(title, description, breadcrumb, icon); 532 } 533 534 @Override 535 public GuidanceStylist onCreateGuidanceStylist() { 536 return new GuidanceStylist() { 537 @Override 538 public int onProvideLayoutId() { 539 return R.layout.guidedstep_second_guidance; 540 } 541 }; 542 } 543 544 @Override 545 public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { 546 String desc = "The description can be quite long as well. " 547 + "Just be sure to set multilineDescription to true in the GuidedAction." 548 + "For testing purpose we make this line even longer since " 549 + "multilineDescriptionMinLines will be set to 2."; 550 actions.add(new GuidedAction.Builder(getActivity()) 551 .title("Note that Guided Actions can have titles that are quite long.") 552 .description(desc) 553 .multilineDescription(true) 554 .infoOnly(true) 555 .enabled(true) 556 .focusable(false) 557 .build()); 558 for (int i = 0; i < OPTION_NAMES.length; i++) { 559 addCheckedAction(actions, RADIO_ID_BASE + i, OPTION_NAMES[i], 560 OPTION_DESCRIPTIONS[i], GuidedAction.DEFAULT_CHECK_SET_ID); 561 if (i == DEFAULT_OPTION) { 562 actions.get(actions.size() -1).setChecked(true); 563 } 564 } 565 for (int i = 0; i < OPTION_NAMES.length; i++) { 566 addCheckedAction(actions, CHECKBOX_ID_BASE + i, OPTION_NAMES[i], 567 OPTION_DESCRIPTIONS[i], GuidedAction.CHECKBOX_CHECK_SET_ID); 568 } 569 } 570 571 @Override 572 public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { 573 actions.add(new GuidedAction.Builder(getActivity()) 574 .clickAction(GuidedAction.ACTION_ID_CONTINUE) 575 .build()); 576 } 577 578 @Override 579 public void onGuidedActionClicked(GuidedAction action) { 580 if (action.getId() == GuidedAction.ACTION_ID_CONTINUE) { 581 FragmentManager fm = getFragmentManager(); 582 FourthStepFragment f = new FourthStepFragment(); 583 Bundle arguments = new Bundle(); 584 arguments.putLong(FourthStepFragment.EXTRA_OPTION, mSelectedOption); 585 f.setArguments(arguments); 586 GuidedStepFragment.add(fm, f, R.id.lb_guidedstep_host); 587 } else if (action.getCheckSetId() == GuidedAction.DEFAULT_CHECK_SET_ID) { 588 mSelectedOption = action.getId(); 589 } 590 } 591 592 } 593 594 public static class FourthStepFragment extends GuidedStepFragment { 595 public static final String EXTRA_OPTION = "extra_option"; 596 597 public FourthStepFragment() { 598 } 599 600 public long getOption() { 601 Bundle b = getArguments(); 602 if (b == null) return 0; 603 return b.getLong(EXTRA_OPTION, 0); 604 } 605 606 @Override 607 public Guidance onCreateGuidance(Bundle savedInstanceState) { 608 String title = getString(R.string.guidedstep_fourth_title); 609 String breadcrumb = getString(R.string.guidedstep_fourth_breadcrumb); 610 String description = "You chose: " + OPTION_NAMES[(int) getOption()]; 611 final Context context = getActivity(); 612 Drawable icon = ResourcesCompat.getDrawable(context.getResources(), 613 R.drawable.ic_main_icon, context.getTheme()); 614 return new Guidance(title, description, breadcrumb, icon); 615 } 616 617 @Override 618 public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { 619 actions.add(new GuidedAction.Builder(getActivity()) 620 .clickAction(GuidedAction.ACTION_ID_FINISH) 621 .description("All Done...") 622 .build()); 623 addAction(actions, BACK, "Start Over", "Let's try this again..."); 624 } 625 626 @Override 627 public void onGuidedActionClicked(GuidedAction action) { 628 if (action.getId() == GuidedAction.ACTION_ID_FINISH) { 629 finishGuidedStepFragments(); 630 } else if (action.getId() == BACK) { 631 // pop 4, 3, 2 632 popBackStackToGuidedStepFragment(SecondStepFragment.class, 633 FragmentManager.POP_BACK_STACK_INCLUSIVE); 634 } 635 } 636 637 } 638 639} 640