UIControllerBase.java revision 627bc6ed57ee06cc588e64ff959bfd7870b659b6
1/* 2 * Copyright (C) 2011 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.email.activity; 18 19import android.app.Activity; 20import android.app.Fragment; 21import android.app.FragmentManager; 22import android.app.FragmentTransaction; 23import android.os.Bundle; 24import android.util.Log; 25import android.view.Menu; 26import android.view.MenuInflater; 27import android.view.MenuItem; 28 29import com.android.email.Email; 30import com.android.email.R; 31import com.android.email.RefreshManager; 32import com.android.email.activity.setup.AccountSettings; 33import com.android.emailcommon.Logging; 34import com.android.emailcommon.provider.Account; 35import com.android.emailcommon.provider.EmailContent.Message; 36import com.android.emailcommon.provider.Mailbox; 37import com.android.emailcommon.utility.EmailAsyncTask; 38 39import java.util.LinkedList; 40import java.util.List; 41 42/** 43 * Base class for the UI controller. 44 */ 45abstract class UIControllerBase implements MailboxListFragment.Callback, 46 MessageListFragment.Callback, MessageViewFragment.Callback { 47 static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE 48 49 protected static final String BUNDLE_KEY_RESUME_INBOX_LOOKUP 50 = "UIController.state.resumeInboxLookup"; 51 protected static final String BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID 52 = "UIController.state.inboxLookupAccountId"; 53 54 /** The owner activity */ 55 final EmailActivity mActivity; 56 57 private final ActionBarController mActionBarController; 58 59 final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 60 61 final RefreshManager mRefreshManager; 62 63 /** {@code true} if the activity is resumed. */ 64 private boolean mResumed; 65 66 /** 67 * Use to find Inbox. This should only run while the activity is resumed, because otherwise 68 * we may not be able to perform fragment transactions when we get a callback. 69 * See also {@link #mResumeInboxLookup}. 70 */ 71 private MailboxFinder mInboxFinder; 72 73 /** 74 * Account ID passed to {@link #startInboxLookup(long)}. We save it for resuming it in 75 * {@link #onActivityResume()}. 76 */ 77 private long mInboxLookupAccountId; 78 79 /** 80 * We (re)start inbox lookup in {@link #onActivityResume} if it's set. 81 * Set in {@link #onActivityPause()} if it's still running, or {@link #startInboxLookup} is 82 * called before the activity is resumed. 83 */ 84 private boolean mResumeInboxLookup; 85 86 /** 87 * Fragments that are installed. 88 * 89 * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in 90 * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks. 91 * 92 * This means fragments in the back stack are *not* installed. 93 * 94 * We set callbacks to fragments only when they are installed. 95 * 96 * @see FragmentInstallable 97 */ 98 private MailboxListFragment mMailboxListFragment; 99 private MessageListFragment mMessageListFragment; 100 private MessageViewFragment mMessageViewFragment; 101 102 /** 103 * To avoid double-deleting a fragment (which will cause a runtime exception), 104 * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it, 105 * and remove from the list when we actually uninstall it. 106 */ 107 private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>(); 108 109 private final RefreshManager.Listener mRefreshListener 110 = new RefreshManager.Listener() { 111 @Override 112 public void onMessagingError(final long accountId, long mailboxId, final String message) { 113 refreshActionBar(); 114 } 115 116 @Override 117 public void onRefreshStatusChanged(long accountId, long mailboxId) { 118 refreshActionBar(); 119 } 120 }; 121 122 public UIControllerBase(EmailActivity activity) { 123 mActivity = activity; 124 mRefreshManager = RefreshManager.getInstance(mActivity); 125 mActionBarController = createActionBarController(activity); 126 if (DEBUG_FRAGMENTS) { 127 FragmentManager.enableDebugLogging(true); 128 } 129 } 130 131 /** 132 * Called by the base class to let a subclass create an {@link ActionBarController}. 133 */ 134 protected abstract ActionBarController createActionBarController(Activity activity); 135 136 /** @return the layout ID for the activity. */ 137 public abstract int getLayoutId(); 138 139 /** 140 * Must be called just after the activity sets up the content view. Used to initialize views. 141 * 142 * (Due to the complexity regarding class/activity initialization order, we can't do this in 143 * the constructor.) 144 */ 145 public void onActivityViewReady() { 146 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 147 Log.d(Logging.LOG_TAG, this + " onActivityViewReady"); 148 } 149 } 150 151 /** 152 * Called at the end of {@link EmailActivity#onCreate}. 153 */ 154 public void onActivityCreated() { 155 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 156 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 157 } 158 mRefreshManager.registerListener(mRefreshListener); 159 mActionBarController.onActivityCreated(); 160 } 161 162 /** 163 * Handles the {@link android.app.Activity#onStart} callback. 164 */ 165 public void onActivityStart() { 166 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 167 Log.d(Logging.LOG_TAG, this + " onActivityStart"); 168 } 169 } 170 171 /** 172 * Handles the {@link android.app.Activity#onResume} callback. 173 */ 174 public void onActivityResume() { 175 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 176 Log.d(Logging.LOG_TAG, this + " onActivityResume"); 177 } 178 mResumed = true; 179 if (mResumeInboxLookup) { 180 startInboxLookup(mInboxLookupAccountId); 181 mResumeInboxLookup = false; 182 } 183 } 184 185 /** 186 * Handles the {@link android.app.Activity#onPause} callback. 187 */ 188 public void onActivityPause() { 189 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 190 Log.d(Logging.LOG_TAG, this + " onActivityPause"); 191 } 192 mResumeInboxLookup = (mInboxFinder != null); 193 stopInboxLookup(); 194 } 195 196 /** 197 * Handles the {@link android.app.Activity#onStop} callback. 198 */ 199 public void onActivityStop() { 200 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 201 Log.d(Logging.LOG_TAG, this + " onActivityStop"); 202 } 203 mResumed = false; 204 } 205 206 /** 207 * Handles the {@link android.app.Activity#onDestroy} callback. 208 */ 209 public void onActivityDestroy() { 210 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 211 Log.d(Logging.LOG_TAG, this + " onActivityDestroy"); 212 } 213 mActionBarController.onActivityDestroy(); 214 mRefreshManager.unregisterListener(mRefreshListener); 215 mTaskTracker.cancellAllInterrupt(); 216 } 217 218 /** 219 * Handles the {@link android.app.Activity#onSaveInstanceState} callback. 220 */ 221 public void onSaveInstanceState(Bundle outState) { 222 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 223 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 224 } 225 outState.putBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP, mResumeInboxLookup); 226 outState.putLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID, mInboxLookupAccountId); 227 mActionBarController.onSaveInstanceState(outState); 228 } 229 230 /** 231 * Handles the {@link android.app.Activity#onRestoreInstanceState} callback. 232 */ 233 public void onRestoreInstanceState(Bundle savedInstanceState) { 234 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 235 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 236 } 237 mResumeInboxLookup = savedInstanceState.getBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP); 238 mInboxLookupAccountId = savedInstanceState.getLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID); 239 mActionBarController.onRestoreInstanceState(savedInstanceState); 240 } 241 242 /** 243 * Install a fragment. Must be caleld from the host activity's 244 * {@link FragmentInstallable#onInstallFragment}. 245 */ 246 public final void onInstallFragment(Fragment fragment) { 247 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 248 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 249 } 250 if (fragment instanceof MailboxListFragment) { 251 installMailboxListFragment((MailboxListFragment) fragment); 252 } else if (fragment instanceof MessageListFragment) { 253 installMessageListFragment((MessageListFragment) fragment); 254 } else if (fragment instanceof MessageViewFragment) { 255 installMessageViewFragment((MessageViewFragment) fragment); 256 } else { 257 throw new IllegalArgumentException("Tried to install unknown fragment"); 258 } 259 } 260 261 /** Install fragment */ 262 protected void installMailboxListFragment(MailboxListFragment fragment) { 263 mMailboxListFragment = fragment; 264 mMailboxListFragment.setCallback(this); 265 refreshActionBar(); 266 } 267 268 /** Install fragment */ 269 protected void installMessageListFragment(MessageListFragment fragment) { 270 mMessageListFragment = fragment; 271 mMessageListFragment.setCallback(this); 272 refreshActionBar(); 273 } 274 275 /** Install fragment */ 276 protected void installMessageViewFragment(MessageViewFragment fragment) { 277 mMessageViewFragment = fragment; 278 mMessageViewFragment.setCallback(this); 279 refreshActionBar(); 280 } 281 282 /** 283 * Uninstall a fragment. Must be caleld from the host activity's 284 * {@link FragmentInstallable#onUninstallFragment}. 285 */ 286 public final void onUninstallFragment(Fragment fragment) { 287 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 288 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 289 } 290 mRemovedFragments.remove(fragment); 291 if (fragment == mMailboxListFragment) { 292 uninstallMailboxListFragment(); 293 } else if (fragment == mMessageListFragment) { 294 uninstallMessageListFragment(); 295 } else if (fragment == mMessageViewFragment) { 296 uninstallMessageViewFragment(); 297 } else { 298 throw new IllegalArgumentException("Tried to uninstall unknown fragment"); 299 } 300 } 301 302 /** Uninstall {@link MailboxListFragment} */ 303 protected void uninstallMailboxListFragment() { 304 mMailboxListFragment.setCallback(null); 305 mMailboxListFragment = null; 306 } 307 308 /** Uninstall {@link MessageListFragment} */ 309 protected void uninstallMessageListFragment() { 310 mMessageListFragment.setCallback(null); 311 mMessageListFragment = null; 312 } 313 314 /** Uninstall {@link MessageViewFragment} */ 315 protected void uninstallMessageViewFragment() { 316 mMessageViewFragment.setCallback(null); 317 mMessageViewFragment = null; 318 } 319 320 /** 321 * If a {@link Fragment} is not already in {@link #mRemovedFragments}, 322 * {@link FragmentTransaction#remove} it and add to the list. 323 * 324 * Do nothing if {@code fragment} is null. 325 */ 326 protected final void removeFragment(FragmentTransaction ft, Fragment fragment) { 327 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 328 Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment); 329 } 330 if (fragment == null) { 331 return; 332 } 333 if (!mRemovedFragments.contains(fragment)) { 334 ft.remove(fragment); 335 addFragmentToRemovalList(fragment); 336 } 337 } 338 339 /** 340 * Remove a {@link Fragment} from {@link #mRemovedFragments}. No-op if {@code fragment} is 341 * null. 342 * 343 * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and 344 * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when 345 * using them. 346 * 347 * However, unfortunately, subclasses have to call this manually when popping from the 348 * back stack to avoid double-delete. 349 */ 350 protected void addFragmentToRemovalList(Fragment fragment) { 351 if (fragment != null) { 352 mRemovedFragments.add(fragment); 353 } 354 } 355 356 /** 357 * Remove the fragment if it's installed. 358 */ 359 protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) { 360 removeFragment(ft, mMailboxListFragment); 361 return ft; 362 } 363 364 /** 365 * Remove the fragment if it's installed. 366 */ 367 protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) { 368 removeFragment(ft, mMessageListFragment); 369 return ft; 370 } 371 372 /** 373 * Remove the fragment if it's installed. 374 */ 375 protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) { 376 removeFragment(ft, mMessageViewFragment); 377 return ft; 378 } 379 380 /** @return true if a {@link MailboxListFragment} is installed. */ 381 protected final boolean isMailboxListInstalled() { 382 return mMailboxListFragment != null; 383 } 384 385 /** @return true if a {@link MessageListFragment} is installed. */ 386 protected final boolean isMessageListInstalled() { 387 return mMessageListFragment != null; 388 } 389 390 /** @return true if a {@link MessageViewFragment} is installed. */ 391 protected final boolean isMessageViewInstalled() { 392 return mMessageViewFragment != null; 393 } 394 395 /** @return the installed {@link MailboxListFragment} or null. */ 396 protected final MailboxListFragment getMailboxListFragment() { 397 return mMailboxListFragment; 398 } 399 400 /** @return the installed {@link MessageListFragment} or null. */ 401 protected final MessageListFragment getMessageListFragment() { 402 return mMessageListFragment; 403 } 404 405 /** @return the installed {@link MessageViewFragment} or null. */ 406 protected final MessageViewFragment getMessageViewFragment() { 407 return mMessageViewFragment; 408 } 409 410 /** 411 * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 412 * 413 * @see #getActualAccountId() 414 */ 415 public abstract long getUIAccountId(); 416 417 /** 418 * @return true if an account is selected, or the current view is the combined view. 419 */ 420 public final boolean isAccountSelected() { 421 return getUIAccountId() != Account.NO_ACCOUNT; 422 } 423 424 /** 425 * @return if an actual account is selected. (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW} 426 * is not considered "actual".s) 427 */ 428 public final boolean isActualAccountSelected() { 429 return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW); 430 } 431 432 /** 433 * @return the currently selected account ID. If the current view is the combined view, 434 * it'll return {@link Account#NO_ACCOUNT}. 435 * 436 * @see #getUIAccountId() 437 */ 438 public final long getActualAccountId() { 439 return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT; 440 } 441 442 /** 443 * Show the default view for the given account. 444 * 445 * No-op if the given account is already selected. 446 * 447 * @param accountId ID of the account to load. Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 448 * Must never be {@link Account#NO_ACCOUNT}. 449 */ 450 public final void switchAccount(long accountId) { 451 if (accountId == getUIAccountId()) { 452 // Do nothing if the account is already selected. Not even going back to the inbox. 453 return; 454 } 455 openAccount(accountId); 456 } 457 458 /** 459 * Shortcut for {@link #open} with {@link Mailbox#NO_MAILBOX} and {@link Message#NO_MESSAGE}. 460 */ 461 protected final void openAccount(long accountId) { 462 open(accountId, Mailbox.NO_MAILBOX, Message.NO_MESSAGE); 463 } 464 465 /** 466 * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}. 467 */ 468 protected final void openMailbox(long accountId, long mailboxId) { 469 open(accountId, mailboxId, Message.NO_MESSAGE); 470 } 471 472 /** 473 * Loads the given account and optionally selects the given mailbox and message. Used to open 474 * a particular view at a request from outside of the activity, such as the widget. 475 * 476 * @param accountId ID of the account to load. Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 477 * Must never be {@link Account#NO_ACCOUNT}. 478 * @param mailboxId ID of the mailbox to load. If {@link Mailbox#NO_MAILBOX}, 479 * load the account's inbox. 480 * @param messageId ID of the message to load. If {@link Message#NO_MESSAGE}, 481 * do not open a message. 482 */ 483 public abstract void open(long accountId, long mailboxId, long messageId); 484 485 /** 486 * Performs the back action. 487 * 488 * NOTE The method in the base class has precedence. Subclasses overriding this method MUST 489 * call super's method first. 490 * 491 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 492 * <code>false</code> if it's caused by the "home" icon click on the action bar. 493 */ 494 public boolean onBackPressed(boolean isSystemBackKey) { 495 if (mActionBarController.onBackPressed(isSystemBackKey)) { 496 return true; 497 } 498 return false; 499 } 500 501 /** 502 * Must be called from {@link Activity#onSearchRequested()}. 503 */ 504 public void onSearchRequested() { 505 mActionBarController.enterSearchMode(null); 506 } 507 508 /** 509 * Callback called when the inbox lookup (started by {@link #startInboxLookup}) is finished. 510 */ 511 protected abstract MailboxFinder.Callback getInboxLookupCallback(); 512 513 private final MailboxFinder.Callback mMailboxFinderCallback = new MailboxFinder.Callback() { 514 private void cleanUp() { 515 mInboxFinder = null; 516 } 517 518 @Override 519 public void onAccountNotFound() { 520 getInboxLookupCallback().onAccountNotFound(); 521 cleanUp(); 522 } 523 524 @Override 525 public void onAccountSecurityHold(long accountId) { 526 getInboxLookupCallback().onAccountSecurityHold(accountId); 527 cleanUp(); 528 } 529 530 @Override 531 public void onMailboxFound(long accountId, long mailboxId) { 532 getInboxLookupCallback().onMailboxFound(accountId, mailboxId); 533 cleanUp(); 534 } 535 536 @Override 537 public void onMailboxNotFound(long accountId) { 538 getInboxLookupCallback().onMailboxNotFound(accountId); 539 cleanUp(); 540 } 541 }; 542 543 /** 544 * Start inbox lookup. 545 */ 546 protected void startInboxLookup(long accountId) { 547 if (mInboxFinder != null) { 548 return; // already running 549 } 550 mInboxLookupAccountId = accountId; 551 if (!mResumed) { 552 mResumeInboxLookup = true; // Don't start yet. 553 return; 554 } 555 mInboxFinder = new MailboxFinder(mActivity, accountId, Mailbox.TYPE_INBOX, 556 mMailboxFinderCallback); 557 mInboxFinder.startLookup(); 558 } 559 560 /** 561 * Stop inbox lookup. 562 */ 563 protected void stopInboxLookup() { 564 if (mInboxFinder == null) { 565 return; // not running 566 } 567 mInboxFinder.cancel(); 568 mInboxFinder = null; 569 } 570 571 /** @return true if the search menu option should be enabled. */ 572 protected boolean canSearch() { 573 return false; 574 } 575 576 /** 577 * Handles the {@link android.app.Activity#onCreateOptionsMenu} callback. 578 */ 579 public boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) { 580 inflater.inflate(R.menu.email_activity_options, menu); 581 return true; 582 } 583 584 /** 585 * Handles the {@link android.app.Activity#onPrepareOptionsMenu} callback. 586 */ 587 public boolean onPrepareOptionsMenu(MenuInflater inflater, Menu menu) { 588 589 // Update the refresh button. 590 MenuItem item = menu.findItem(R.id.refresh); 591 if (isRefreshEnabled()) { 592 item.setVisible(true); 593 if (isRefreshInProgress()) { 594 item.setActionView(R.layout.action_bar_indeterminate_progress); 595 } else { 596 item.setActionView(null); 597 } 598 } else { 599 item.setVisible(false); 600 } 601 602 // STOPSHIP Temporary search options code 603 // Only show search/sync options for EAS 12.0 and later 604 boolean canSearch = false; 605 if (canSearch()) { 606 long accountId = getActualAccountId(); 607 if (accountId > 0) { 608 // Move database operations out of the UI thread 609 if ("eas".equals(Account.getProtocol(mActivity, accountId))) { 610 Account account = Account.restoreAccountWithId(mActivity, accountId); 611 if (account != null) { 612 // We should set a flag in the account indicating ability to handle search 613 String protocolVersion = account.mProtocolVersion; 614 if (Double.parseDouble(protocolVersion) >= 12.0) { 615 canSearch = true; 616 } 617 } 618 } 619 } 620 } 621 // Should use an isSearchable call to prevent search on inappropriate accounts/boxes 622 // STOPSHIP Figure out where the "canSearch" test belongs 623 menu.findItem(R.id.search).setVisible(true); //canSearch); 624 625 return true; 626 } 627 628 /** 629 * Handles the {@link android.app.Activity#onOptionsItemSelected} callback. 630 * 631 * @return true if the option item is handled. 632 */ 633 public boolean onOptionsItemSelected(MenuItem item) { 634 switch (item.getItemId()) { 635 case android.R.id.home: 636 // Comes from the action bar when the app icon on the left is pressed. 637 // It works like a back press, but it won't close the activity. 638 return onBackPressed(false); 639 case R.id.compose: 640 return onCompose(); 641 case R.id.refresh: 642 onRefresh(); 643 return true; 644 case R.id.account_settings: 645 return onAccountSettings(); 646 case R.id.search: 647 onSearchRequested(); 648 return true; 649 } 650 return false; 651 } 652 653 /** 654 * Opens the message compose activity. 655 */ 656 private boolean onCompose() { 657 if (!isAccountSelected()) { 658 return false; // this shouldn't really happen 659 } 660 MessageCompose.actionCompose(mActivity, getActualAccountId()); 661 return true; 662 } 663 664 /** 665 * Handles the "Settings" option item. Opens the settings activity. 666 */ 667 private boolean onAccountSettings() { 668 AccountSettings.actionSettings(mActivity, getActualAccountId()); 669 return true; 670 } 671 672 /** 673 * STOPSHIP For experimental UI. Remove this. 674 * 675 * @return mailbox ID for "mailbox settings" option. 676 */ 677 public abstract long getMailboxSettingsMailboxId(); 678 679 /** 680 * STOPSHIP For experimental UI. Make it abstract protected. 681 * 682 * Performs "refesh". 683 */ 684 public abstract void onRefresh(); 685 686 /** 687 * @return true if refresh is in progress for the current mailbox. 688 */ 689 protected abstract boolean isRefreshInProgress(); 690 691 /** 692 * @return true if the UI should enable the "refresh" command. 693 */ 694 protected abstract boolean isRefreshEnabled(); 695 696 /** 697 * Refresh the action bar and menu items, including the "refreshing" icon. 698 */ 699 protected void refreshActionBar() { 700 if (mActionBarController != null) { 701 mActionBarController.refresh(); 702 } 703 mActivity.invalidateOptionsMenu(); 704 } 705 706 @Override 707 public String toString() { 708 return getClass().getSimpleName(); // Shown on logcat 709 } 710} 711