UIControllerBase.java revision 99401ebea71f4ff15fc017edd0ab98cad05bbcd6
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.AccountSecurity; 33import com.android.email.activity.setup.AccountSettings; 34import com.android.emailcommon.Logging; 35import com.android.emailcommon.provider.Account; 36import com.android.emailcommon.provider.EmailContent.Message; 37import com.android.emailcommon.provider.Mailbox; 38import com.android.emailcommon.utility.EmailAsyncTask; 39 40import java.util.LinkedList; 41import java.util.List; 42 43/** 44 * Base class for the UI controller. 45 */ 46abstract class UIControllerBase implements MailboxListFragment.Callback, 47 MessageListFragment.Callback, MessageViewFragment.Callback { 48 static final boolean DEBUG_FRAGMENTS = false; // DO NOT SUBMIT WITH TRUE 49 50 protected static final String BUNDLE_KEY_RESUME_INBOX_LOOKUP 51 = "UIController.state.resumeInboxLookup"; 52 protected static final String BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID 53 = "UIController.state.inboxLookupAccountId"; 54 55 /** The owner activity */ 56 final EmailActivity mActivity; 57 final FragmentManager mFragmentManager; 58 59 private final ActionBarController mActionBarController; 60 61 final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 62 63 final RefreshManager mRefreshManager; 64 65 /** {@code true} if the activity is resumed. */ 66 private boolean mResumed; 67 68 /** 69 * Use to find Inbox. This should only run while the activity is resumed, because otherwise 70 * we may not be able to perform fragment transactions when we get a callback. 71 * See also {@link #mResumeInboxLookup}. 72 */ 73 private MailboxFinder mInboxFinder; 74 75 /** 76 * Account ID passed to {@link #startInboxLookup(long)}. We save it for resuming it in 77 * {@link #onActivityResume()}. 78 */ 79 private long mInboxLookupAccountId; 80 81 /** 82 * We (re)start inbox lookup in {@link #onActivityResume} if it's set. 83 * Set in {@link #onActivityPause()} if it's still running, or {@link #startInboxLookup} is 84 * called before the activity is resumed. 85 */ 86 private boolean mResumeInboxLookup; 87 88 /** 89 * Fragments that are installed. 90 * 91 * A fragment is installed in {@link Fragment#onActivityCreated} and uninstalled in 92 * {@link Fragment#onDestroyView}, using {@link FragmentInstallable} callbacks. 93 * 94 * This means fragments in the back stack are *not* installed. 95 * 96 * We set callbacks to fragments only when they are installed. 97 * 98 * @see FragmentInstallable 99 */ 100 private MailboxListFragment mMailboxListFragment; 101 private MessageListFragment mMessageListFragment; 102 private MessageViewFragment mMessageViewFragment; 103 104 /** 105 * To avoid double-deleting a fragment (which will cause a runtime exception), 106 * we put a fragment in this list when we {@link FragmentTransaction#remove(Fragment)} it, 107 * and remove from the list when we actually uninstall it. 108 */ 109 private final List<Fragment> mRemovedFragments = new LinkedList<Fragment>(); 110 111 private final RefreshManager.Listener mRefreshListener 112 = new RefreshManager.Listener() { 113 @Override 114 public void onMessagingError(final long accountId, long mailboxId, final String message) { 115 refreshActionBar(); 116 } 117 118 @Override 119 public void onRefreshStatusChanged(long accountId, long mailboxId) { 120 refreshActionBar(); 121 } 122 }; 123 124 public UIControllerBase(EmailActivity activity) { 125 mActivity = activity; 126 mFragmentManager = activity.getFragmentManager(); 127 mRefreshManager = RefreshManager.getInstance(mActivity); 128 mActionBarController = createActionBarController(activity); 129 if (DEBUG_FRAGMENTS) { 130 FragmentManager.enableDebugLogging(true); 131 } 132 } 133 134 /** 135 * Called by the base class to let a subclass create an {@link ActionBarController}. 136 */ 137 protected abstract ActionBarController createActionBarController(Activity activity); 138 139 /** @return the layout ID for the activity. */ 140 public abstract int getLayoutId(); 141 142 /** 143 * Must be called just after the activity sets up the content view. Used to initialize views. 144 * 145 * (Due to the complexity regarding class/activity initialization order, we can't do this in 146 * the constructor.) 147 */ 148 public void onActivityViewReady() { 149 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 150 Log.d(Logging.LOG_TAG, this + " onActivityViewReady"); 151 } 152 } 153 154 /** 155 * Called at the end of {@link EmailActivity#onCreate}. 156 */ 157 public void onActivityCreated() { 158 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 159 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 160 } 161 mRefreshManager.registerListener(mRefreshListener); 162 mActionBarController.onActivityCreated(); 163 } 164 165 /** 166 * Handles the {@link android.app.Activity#onStart} callback. 167 */ 168 public void onActivityStart() { 169 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 170 Log.d(Logging.LOG_TAG, this + " onActivityStart"); 171 } 172 } 173 174 /** 175 * Handles the {@link android.app.Activity#onResume} callback. 176 */ 177 public void onActivityResume() { 178 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 179 Log.d(Logging.LOG_TAG, this + " onActivityResume"); 180 } 181 mResumed = true; 182 if (mResumeInboxLookup) { 183 startInboxLookup(mInboxLookupAccountId); 184 mResumeInboxLookup = false; 185 } 186 } 187 188 /** 189 * Handles the {@link android.app.Activity#onPause} callback. 190 */ 191 public void onActivityPause() { 192 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 193 Log.d(Logging.LOG_TAG, this + " onActivityPause"); 194 } 195 mResumeInboxLookup = (mInboxFinder != null); 196 stopInboxLookup(); 197 } 198 199 /** 200 * Handles the {@link android.app.Activity#onStop} callback. 201 */ 202 public void onActivityStop() { 203 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 204 Log.d(Logging.LOG_TAG, this + " onActivityStop"); 205 } 206 mResumed = false; 207 } 208 209 /** 210 * Handles the {@link android.app.Activity#onDestroy} callback. 211 */ 212 public void onActivityDestroy() { 213 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 214 Log.d(Logging.LOG_TAG, this + " onActivityDestroy"); 215 } 216 mActionBarController.onActivityDestroy(); 217 mRefreshManager.unregisterListener(mRefreshListener); 218 mTaskTracker.cancellAllInterrupt(); 219 } 220 221 /** 222 * Handles the {@link android.app.Activity#onSaveInstanceState} callback. 223 */ 224 public void onSaveInstanceState(Bundle outState) { 225 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 226 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 227 } 228 outState.putBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP, mResumeInboxLookup); 229 outState.putLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID, mInboxLookupAccountId); 230 mActionBarController.onSaveInstanceState(outState); 231 } 232 233 /** 234 * Handles the {@link android.app.Activity#onRestoreInstanceState} callback. 235 */ 236 public void onRestoreInstanceState(Bundle savedInstanceState) { 237 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 238 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 239 } 240 mResumeInboxLookup = savedInstanceState.getBoolean(BUNDLE_KEY_RESUME_INBOX_LOOKUP); 241 mInboxLookupAccountId = savedInstanceState.getLong(BUNDLE_KEY_INBOX_LOOKUP_ACCOUNT_ID); 242 mActionBarController.onRestoreInstanceState(savedInstanceState); 243 } 244 245 /** 246 * Install a fragment. Must be caleld from the host activity's 247 * {@link FragmentInstallable#onInstallFragment}. 248 */ 249 public final void onInstallFragment(Fragment fragment) { 250 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 251 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 252 } 253 if (fragment instanceof MailboxListFragment) { 254 installMailboxListFragment((MailboxListFragment) fragment); 255 } else if (fragment instanceof MessageListFragment) { 256 installMessageListFragment((MessageListFragment) fragment); 257 } else if (fragment instanceof MessageViewFragment) { 258 installMessageViewFragment((MessageViewFragment) fragment); 259 } else { 260 throw new IllegalArgumentException("Tried to install unknown fragment"); 261 } 262 } 263 264 /** Install fragment */ 265 protected void installMailboxListFragment(MailboxListFragment fragment) { 266 mMailboxListFragment = fragment; 267 mMailboxListFragment.setCallback(this); 268 refreshActionBar(); 269 } 270 271 /** Install fragment */ 272 protected void installMessageListFragment(MessageListFragment fragment) { 273 mMessageListFragment = fragment; 274 mMessageListFragment.setCallback(this); 275 refreshActionBar(); 276 } 277 278 /** Install fragment */ 279 protected void installMessageViewFragment(MessageViewFragment fragment) { 280 mMessageViewFragment = fragment; 281 mMessageViewFragment.setCallback(this); 282 refreshActionBar(); 283 } 284 285 /** 286 * Uninstall a fragment. Must be caleld from the host activity's 287 * {@link FragmentInstallable#onUninstallFragment}. 288 */ 289 public final void onUninstallFragment(Fragment fragment) { 290 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 291 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 292 } 293 mRemovedFragments.remove(fragment); 294 if (fragment == mMailboxListFragment) { 295 uninstallMailboxListFragment(); 296 } else if (fragment == mMessageListFragment) { 297 uninstallMessageListFragment(); 298 } else if (fragment == mMessageViewFragment) { 299 uninstallMessageViewFragment(); 300 } else { 301 throw new IllegalArgumentException("Tried to uninstall unknown fragment"); 302 } 303 } 304 305 /** Uninstall {@link MailboxListFragment} */ 306 protected void uninstallMailboxListFragment() { 307 mMailboxListFragment.setCallback(null); 308 mMailboxListFragment = null; 309 } 310 311 /** Uninstall {@link MessageListFragment} */ 312 protected void uninstallMessageListFragment() { 313 mMessageListFragment.setCallback(null); 314 mMessageListFragment = null; 315 } 316 317 /** Uninstall {@link MessageViewFragment} */ 318 protected void uninstallMessageViewFragment() { 319 mMessageViewFragment.setCallback(null); 320 mMessageViewFragment = null; 321 } 322 323 /** 324 * If a {@link Fragment} is not already in {@link #mRemovedFragments}, 325 * {@link FragmentTransaction#remove} it and add to the list. 326 * 327 * Do nothing if {@code fragment} is null. 328 */ 329 protected final void removeFragment(FragmentTransaction ft, Fragment fragment) { 330 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 331 Log.d(Logging.LOG_TAG, this + " removeFragment fragment=" + fragment); 332 } 333 if (fragment == null) { 334 return; 335 } 336 if (!mRemovedFragments.contains(fragment)) { 337 ft.remove(fragment); 338 addFragmentToRemovalList(fragment); 339 } 340 } 341 342 /** 343 * Remove a {@link Fragment} from {@link #mRemovedFragments}. No-op if {@code fragment} is 344 * null. 345 * 346 * {@link #removeMailboxListFragment}, {@link #removeMessageListFragment} and 347 * {@link #removeMessageViewFragment} all call this, so subclasses don't have to do this when 348 * using them. 349 * 350 * However, unfortunately, subclasses have to call this manually when popping from the 351 * back stack to avoid double-delete. 352 */ 353 protected void addFragmentToRemovalList(Fragment fragment) { 354 if (fragment != null) { 355 mRemovedFragments.add(fragment); 356 } 357 } 358 359 /** 360 * Remove the fragment if it's installed. 361 */ 362 protected FragmentTransaction removeMailboxListFragment(FragmentTransaction ft) { 363 removeFragment(ft, mMailboxListFragment); 364 return ft; 365 } 366 367 /** 368 * Remove the fragment if it's installed. 369 */ 370 protected FragmentTransaction removeMessageListFragment(FragmentTransaction ft) { 371 removeFragment(ft, mMessageListFragment); 372 return ft; 373 } 374 375 /** 376 * Remove the fragment if it's installed. 377 */ 378 protected FragmentTransaction removeMessageViewFragment(FragmentTransaction ft) { 379 removeFragment(ft, mMessageViewFragment); 380 return ft; 381 } 382 383 /** @return true if a {@link MailboxListFragment} is installed. */ 384 protected final boolean isMailboxListInstalled() { 385 return mMailboxListFragment != null; 386 } 387 388 /** @return true if a {@link MessageListFragment} is installed. */ 389 protected final boolean isMessageListInstalled() { 390 return mMessageListFragment != null; 391 } 392 393 /** @return true if a {@link MessageViewFragment} is installed. */ 394 protected final boolean isMessageViewInstalled() { 395 return mMessageViewFragment != null; 396 } 397 398 /** @return the installed {@link MailboxListFragment} or null. */ 399 protected final MailboxListFragment getMailboxListFragment() { 400 return mMailboxListFragment; 401 } 402 403 /** @return the installed {@link MessageListFragment} or null. */ 404 protected final MessageListFragment getMessageListFragment() { 405 return mMessageListFragment; 406 } 407 408 /** @return the installed {@link MessageViewFragment} or null. */ 409 protected final MessageViewFragment getMessageViewFragment() { 410 return mMessageViewFragment; 411 } 412 413 /** 414 * @return the currently selected account ID, *or* {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 415 * 416 * @see #getActualAccountId() 417 */ 418 public abstract long getUIAccountId(); 419 420 /** 421 * @return true if an account is selected, or the current view is the combined view. 422 */ 423 public final boolean isAccountSelected() { 424 return getUIAccountId() != Account.NO_ACCOUNT; 425 } 426 427 /** 428 * @return if an actual account is selected. (i.e. {@link Account#ACCOUNT_ID_COMBINED_VIEW} 429 * is not considered "actual".s) 430 */ 431 public final boolean isActualAccountSelected() { 432 return isAccountSelected() && (getUIAccountId() != Account.ACCOUNT_ID_COMBINED_VIEW); 433 } 434 435 /** 436 * @return the currently selected account ID. If the current view is the combined view, 437 * it'll return {@link Account#NO_ACCOUNT}. 438 * 439 * @see #getUIAccountId() 440 */ 441 public final long getActualAccountId() { 442 return isActualAccountSelected() ? getUIAccountId() : Account.NO_ACCOUNT; 443 } 444 445 /** 446 * Show the default view for the given account. 447 * 448 * No-op if the given account is already selected. 449 * 450 * @param accountId ID of the account to load. Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 451 * Must never be {@link Account#NO_ACCOUNT}. 452 */ 453 public final void switchAccount(long accountId) { 454 if (accountId == getUIAccountId()) { 455 // Do nothing if the account is already selected. Not even going back to the inbox. 456 return; 457 } 458 459 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 460 open(accountId, Mailbox.QUERY_ALL_INBOXES, Message.NO_MESSAGE); 461 } else { 462 // For a normal account, just open the inbox. Unfortunately, we have to look it up first 463 startInboxLookup(accountId); 464 } 465 } 466 467 /** 468 * Shortcut for {@link #open} with {@link Message#NO_MESSAGE}. 469 */ 470 protected final void openMailbox(long accountId, long mailboxId) { 471 open(accountId, mailboxId, Message.NO_MESSAGE); 472 } 473 474 /** 475 * Loads the given account and optionally selects the given mailbox and message. Used to open 476 * a particular view at a request from outside of the activity, such as the widget. 477 * 478 * @param accountId ID of the account to load. Can be {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 479 * Must never be {@link Account#NO_ACCOUNT}. 480 * @param mailboxId ID of the mailbox to load. Must always be specified and cannot be 481 * {@link Mailbox#NO_MAILBOX} 482 * @param messageId ID of the message to load. If {@link Message#NO_MESSAGE}, 483 * do not open a message. 484 */ 485 public abstract void open(long accountId, long mailboxId, long messageId); 486 487 /** 488 * Performs the back action. 489 * 490 * NOTE The method in the base class has precedence. Subclasses overriding this method MUST 491 * call super's method first. 492 * 493 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 494 * <code>false</code> if it's caused by the "home" icon click on the action bar. 495 */ 496 public boolean onBackPressed(boolean isSystemBackKey) { 497 if (mActionBarController.onBackPressed(isSystemBackKey)) { 498 return true; 499 } 500 return false; 501 } 502 503 /** 504 * Must be called from {@link Activity#onSearchRequested()}. 505 */ 506 public void onSearchRequested() { 507 mActionBarController.enterSearchMode(null); 508 } 509 510 private final MailboxFinder.Callback mMailboxFinderCallback = new MailboxFinder.Callback() { 511 private void cleanUp() { 512 mInboxFinder = null; 513 } 514 515 @Override 516 public void onAccountNotFound() { 517 // Account removed? 518 Welcome.actionStart(mActivity); 519 cleanUp(); 520 } 521 522 @Override 523 public void onAccountSecurityHold(long accountId) { 524 mActivity.startActivity( 525 AccountSecurity.actionUpdateSecurityIntent(mActivity, accountId, true)); 526 cleanUp(); 527 } 528 529 @Override 530 public void onMailboxFound(long accountId, long mailboxId) { 531 openMailbox(accountId, mailboxId); 532 cleanUp(); 533 } 534 535 @Override 536 public void onMailboxNotFound(long accountId) { 537 // Inbox not found. 538 Welcome.actionStart(mActivity); 539 cleanUp(); 540 } 541 }; 542 543 /** 544 * Start inbox lookup. 545 */ 546 private 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 private 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