TwoPaneController.java revision f84fe2eec91f437f02b6944e4a4981d3907652a0
1/******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18package com.android.mail.ui; 19 20import android.app.Activity; 21import android.app.Fragment; 22import android.app.FragmentManager; 23import android.app.FragmentTransaction; 24import android.content.Intent; 25import android.os.Bundle; 26import android.support.annotation.IdRes; 27import android.support.annotation.LayoutRes; 28import android.support.v7.app.ActionBar; 29import android.view.KeyEvent; 30import android.view.View; 31import android.widget.ListView; 32 33import com.android.mail.ConversationListContext; 34import com.android.mail.R; 35import com.android.mail.providers.Account; 36import com.android.mail.providers.Conversation; 37import com.android.mail.providers.Folder; 38import com.android.mail.providers.UIProvider.ConversationListIcon; 39import com.android.mail.utils.LogUtils; 40import com.android.mail.utils.Utils; 41 42/** 43 * Controller for two-pane Mail activity. Two Pane is used for tablets, where screen real estate 44 * abounds. 45 */ 46public final class TwoPaneController extends AbstractActivityController implements 47 ConversationViewFrame.DownEventListener { 48 49 private static final String SAVED_MISCELLANEOUS_VIEW = "saved-miscellaneous-view"; 50 private static final String SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID = 51 "saved-miscellaneous-view-transaction-id"; 52 53 private TwoPaneLayout mLayout; 54 @Deprecated 55 private Conversation mConversationToShow; 56 57 /** 58 * 2-pane, in wider configurations, allows peeking at a conversation view without having the 59 * conversation marked-as-read as far as read/unread state goes.<br> 60 * <br> 61 * This flag applies to {@link AbstractActivityController#mCurrentConversation} and indicates 62 * that the current conversation, if set, is in a 'peeking' state. If there is no current 63 * conversation, peeking is implied (in certain view configurations) and this value is 64 * meaningless. 65 */ 66 // TODO: save in instance state 67 private boolean mCurrentConversationJustPeeking; 68 69 /** 70 * Used to determine whether onViewModeChanged should skip a potential 71 * fragment transaction that would remove a miscellaneous view. 72 */ 73 private boolean mSavedMiscellaneousView = false; 74 75 private boolean mIsTabletLandscape; 76 77 private TwoPaneLayout.ConversationListLayoutListener mConversationListLayoutListener; 78 79 public TwoPaneController(MailActivity activity, ViewMode viewMode) { 80 super(activity, viewMode); 81 } 82 83 public boolean isCurrentConversationJustPeeking() { 84 return mCurrentConversationJustPeeking; 85 } 86 87 private boolean isConversationOnlyMode() { 88 return getCurrentConversation() != null && !isCurrentConversationJustPeeking() 89 && !mLayout.shouldShowPreviewPanel(); 90 } 91 92 /** 93 * Display the conversation list fragment. 94 */ 95 private void initializeConversationListFragment() { 96 if (Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())) { 97 if (shouldEnterSearchConvMode()) { 98 mViewMode.enterSearchResultsConversationMode(); 99 } else { 100 mViewMode.enterSearchResultsListMode(); 101 } 102 } 103 renderConversationList(); 104 } 105 106 /** 107 * Render the conversation list in the correct pane. 108 */ 109 private void renderConversationList() { 110 if (mActivity == null) { 111 return; 112 } 113 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 114 // Use cross fading animation. 115 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); 116 final ConversationListFragment conversationListFragment = 117 ConversationListFragment.newInstance(mConvListContext); 118 fragmentTransaction.replace(R.id.conversation_list_pane, conversationListFragment, 119 TAG_CONVERSATION_LIST); 120 fragmentTransaction.commitAllowingStateLoss(); 121 // Set default navigation here once the ConversationListFragment is created. 122 conversationListFragment.setNextFocusLeftId( 123 getClfNextFocusLeftId(getFolderListFragment().isMinimized())); 124 } 125 126 @Override 127 public boolean doesActionChangeConversationListVisibility(final int action) { 128 if (action == R.id.settings 129 || action == R.id.compose 130 || action == R.id.help_info_menu_item 131 || action == R.id.feedback_menu_item) { 132 return true; 133 } 134 135 return false; 136 } 137 138 @Override 139 protected boolean isConversationListVisible() { 140 return !mLayout.isConversationListCollapsed(); 141 } 142 143 @Override 144 protected void showConversationList(ConversationListContext listContext) { 145 initializeConversationListFragment(); 146 } 147 148 @Override 149 public @LayoutRes int getContentViewResource() { 150 return R.layout.two_pane_activity; 151 } 152 153 @Override 154 public boolean onCreate(Bundle savedState) { 155 mLayout = (TwoPaneLayout) mActivity.findViewById(R.id.two_pane_activity); 156 if (mLayout == null) { 157 // We need the layout for everything. Crash/Return early if it is null. 158 LogUtils.wtf(LOG_TAG, "mLayout is null!"); 159 return false; 160 } 161 mLayout.setConversationListLayoutListener(mConversationListLayoutListener); 162 mLayout.setController(this, Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction())); 163 mActivity.getWindow().setBackgroundDrawable(null); 164 mIsTabletLandscape = !mActivity.getResources().getBoolean(R.bool.list_collapsible); 165 166 final FolderListFragment flf = getFolderListFragment(); 167 flf.setMiniDrawerEnabled(true); 168 flf.setMinimized(true); 169 170 if (savedState != null) { 171 mSavedMiscellaneousView = savedState.getBoolean(SAVED_MISCELLANEOUS_VIEW, false); 172 mMiscellaneousViewTransactionId = 173 savedState.getInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, -1); 174 } 175 176 // 2-pane layout is the main listener of view mode changes, and issues secondary 177 // notifications upon animation completion: 178 // (onConversationVisibilityChanged, onConversationListVisibilityChanged) 179 mViewMode.addListener(mLayout); 180 return super.onCreate(savedState); 181 } 182 183 @Override 184 public void onSaveInstanceState(Bundle outState) { 185 super.onSaveInstanceState(outState); 186 187 outState.putBoolean(SAVED_MISCELLANEOUS_VIEW, mMiscellaneousViewTransactionId >= 0); 188 outState.putInt(SAVED_MISCELLANEOUS_VIEW_TRANSACTION_ID, mMiscellaneousViewTransactionId); 189 } 190 191 @Override 192 public void onWindowFocusChanged(boolean hasFocus) { 193 if (hasFocus && !mLayout.isConversationListCollapsed()) { 194 // The conversation list is visible. 195 informCursorVisiblity(true); 196 } 197 } 198 199 @Override 200 public void switchToDefaultInboxOrChangeAccount(Account account) { 201 if (mViewMode.isSearchMode()) { 202 // We are in an activity on top of the main navigation activity. 203 // We need to return to it with a result code that indicates it should navigate to 204 // a different folder. 205 final Intent intent = new Intent(); 206 intent.putExtra(AbstractActivityController.EXTRA_ACCOUNT, account); 207 mActivity.setResult(Activity.RESULT_OK, intent); 208 mActivity.finish(); 209 return; 210 } 211 if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) { 212 mViewMode.enterConversationListMode(); 213 } 214 super.switchToDefaultInboxOrChangeAccount(account); 215 } 216 217 @Override 218 public void onFolderSelected(Folder folder) { 219 // It's possible that we are not in conversation list mode 220 if (mViewMode.isSearchMode()) { 221 // We are in an activity on top of the main navigation activity. 222 // We need to return to it with a result code that indicates it should navigate to 223 // a different folder. 224 final Intent intent = new Intent(); 225 intent.putExtra(AbstractActivityController.EXTRA_FOLDER, folder); 226 mActivity.setResult(Activity.RESULT_OK, intent); 227 mActivity.finish(); 228 return; 229 } else if (mViewMode.getMode() != ViewMode.CONVERSATION_LIST) { 230 mViewMode.enterConversationListMode(); 231 } 232 233 setHierarchyFolder(folder); 234 super.onFolderSelected(folder); 235 } 236 237 public boolean isDrawerOpen() { 238 final FolderListFragment flf = getFolderListFragment(); 239 return flf != null && !flf.isMinimized(); 240 } 241 242 @Override 243 protected void toggleDrawerState() { 244 final FolderListFragment flf = getFolderListFragment(); 245 if (flf == null) { 246 LogUtils.w(LOG_TAG, "no drawer to toggle open/closed"); 247 return; 248 } 249 flf.setMinimized(!flf.isMinimized()); 250 mLayout.requestLayout(); 251 resetActionBarIcon(); 252 253 final ConversationListFragment clf = getConversationListFragment(); 254 if (clf != null) { 255 clf.setNextFocusLeftId(getClfNextFocusLeftId(flf.isMinimized())); 256 257 final SwipeableListView list = clf.getListView(); 258 if (list != null) { 259 if (flf.isMinimized()) { 260 list.stopPreventingSwipes(); 261 } else { 262 list.preventSwipesEntirely(); 263 } 264 } 265 } 266 } 267 268 @Override 269 public boolean shouldPreventListSwipesEntirely() { 270 return isDrawerOpen(); 271 } 272 273 @Override 274 public void onViewModeChanged(int newMode) { 275 if (!mSavedMiscellaneousView && mMiscellaneousViewTransactionId >= 0) { 276 final FragmentManager fragmentManager = mActivity.getFragmentManager(); 277 fragmentManager.popBackStackImmediate(mMiscellaneousViewTransactionId, 278 FragmentManager.POP_BACK_STACK_INCLUSIVE); 279 mMiscellaneousViewTransactionId = -1; 280 } 281 mSavedMiscellaneousView = false; 282 283 super.onViewModeChanged(newMode); 284 if (newMode != ViewMode.WAITING_FOR_ACCOUNT_INITIALIZATION) { 285 // Clear the wait fragment 286 hideWaitForInitialization(); 287 } 288 // In conversation mode, if the conversation list is not visible, then the user cannot 289 // see the selected conversations. Disable the CAB mode while leaving the selected set 290 // untouched. 291 // When the conversation list is made visible again, try to enable the CAB 292 // mode if any conversations are selected. 293 if (newMode == ViewMode.CONVERSATION || newMode == ViewMode.CONVERSATION_LIST 294 || ViewMode.isAdMode(newMode)) { 295 enableOrDisableCab(); 296 } 297 } 298 299 private @IdRes int getClfNextFocusLeftId(boolean drawerMinimized) { 300 return (drawerMinimized) ? R.id.current_account_avatar : android.R.id.list; 301 } 302 303 @Override 304 public void onConversationVisibilityChanged(boolean visible) { 305 super.onConversationVisibilityChanged(visible); 306 if (!visible) { 307 mPagerController.hide(false /* changeVisibility */); 308 } else if (mConversationToShow != null) { 309 mPagerController.show(mAccount, mFolder, mConversationToShow, 310 false /* changeVisibility */); 311 mConversationToShow = null; 312 } 313 } 314 315 @Override 316 public void onConversationListVisibilityChanged(boolean visible) { 317 super.onConversationListVisibilityChanged(visible); 318 enableOrDisableCab(); 319 } 320 321 @Override 322 public void resetActionBarIcon() { 323 final ActionBar ab = mActivity.getSupportActionBar(); 324 final boolean isChildFolder = getFolder() != null && !Utils.isEmpty(getFolder().parent); 325 if (isConversationOnlyMode() || isChildFolder) { 326 ab.setHomeAsUpIndicator(R.drawable.ic_arrow_back_wht_24dp); 327 ab.setHomeActionContentDescription(0 /* system default */); 328 } else { 329 ab.setHomeAsUpIndicator(R.drawable.ic_drawer); 330 ab.setHomeActionContentDescription( 331 isDrawerOpen() ? R.string.drawer_close : R.string.drawer_open); 332 } 333 } 334 335 @Override 336 protected boolean isComposeVisible(int mode) { 337 return super.isComposeVisible(mode) || 338 (mIsTabletLandscape && mode == ViewMode.CONVERSATION); 339 } 340 341 /** 342 * Enable or disable the CAB mode based on the visibility of the conversation list fragment. 343 */ 344 private void enableOrDisableCab() { 345 if (mLayout.isConversationListCollapsed()) { 346 disableCabMode(); 347 } else { 348 enableCabMode(); 349 } 350 } 351 352 @Override 353 public void onSetPopulated(ConversationSelectionSet set) { 354 super.onSetPopulated(set); 355 356 boolean showSenderImage = 357 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); 358 if (!showSenderImage && mViewMode.isListMode()) { 359 getConversationListFragment().setChoiceNone(); 360 } 361 } 362 363 @Override 364 public void onSetEmpty() { 365 super.onSetEmpty(); 366 367 boolean showSenderImage = 368 (mAccount.settings.convListIcon == ConversationListIcon.SENDER_IMAGE); 369 if (!showSenderImage && mViewMode.isListMode()) { 370 getConversationListFragment().revertChoiceMode(); 371 } 372 } 373 374 @Override 375 protected void showConversation(Conversation conversation, boolean markAsRead) { 376 super.showConversation(conversation, markAsRead); 377 378 // 2-pane can ignore inLoaderCallbacks because it doesn't use 379 // FragmentManager.popBackStack(). 380 381 if (mActivity == null) { 382 return; 383 } 384 if (conversation == null) { 385 handleBackPress(); 386 return; 387 } 388 // If conversation list is not visible, then the user cannot see the CAB mode, so exit it. 389 // This is needed here (in addition to during viewmode changes) because orientation changes 390 // while viewing a conversation don't change the viewmode: the mode stays 391 // ViewMode.CONVERSATION and yet the conversation list goes in and out of visibility. 392 enableOrDisableCab(); 393 394 // close the drawer, if open 395 if (isDrawerOpen()) { 396 toggleDrawerState(); 397 } 398 399 // When a mode change is required, wait for onConversationVisibilityChanged(), the signal 400 // that the mode change animation has finished, before rendering the conversation. 401 mConversationToShow = conversation; 402 mCurrentConversationJustPeeking = !markAsRead; 403 404 final int mode = mViewMode.getMode(); 405 LogUtils.i(LOG_TAG, "IN TPC.showConv, oldMode=%s conv=%s", mode, mConversationToShow); 406 if (mode == ViewMode.SEARCH_RESULTS_LIST || mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 407 mViewMode.enterSearchResultsConversationMode(); 408 } else { 409 mViewMode.enterConversationMode(); 410 } 411 // load the conversation immediately if we're already in conversation mode 412 if (!mLayout.isModeChangePending()) { 413 onConversationVisibilityChanged(true); 414 } else { 415 LogUtils.i(LOG_TAG, "TPC.showConversation will wait for TPL.animationEnd to show!"); 416 } 417 } 418 419 @Override 420 public void onConversationSelected(Conversation conversation, boolean inLoaderCallbacks) { 421 super.onConversationSelected(conversation, inLoaderCallbacks); 422 if (!mCurrentConversationJustPeeking) { 423 // Shift the focus to the conversation in landscape mode. 424 mPagerController.focusPager(); 425 } 426 } 427 428 @Override 429 public void onConversationFocused(Conversation conversation) { 430 if (mIsTabletLandscape) { 431 showConversation(conversation, false /* markAsRead */); 432 } 433 } 434 435 @Override 436 public void setCurrentConversation(Conversation conversation) { 437 // Order is important! We want to calculate different *before* the superclass changes 438 // mCurrentConversation, so before super.setCurrentConversation(). 439 final long oldId = mCurrentConversation != null ? mCurrentConversation.id : -1; 440 final long newId = conversation != null ? conversation.id : -1; 441 final boolean different = oldId != newId; 442 443 // This call might change mCurrentConversation. 444 super.setCurrentConversation(conversation); 445 446 final ConversationListFragment convList = getConversationListFragment(); 447 if (convList != null && conversation != null) { 448 convList.setSelected(conversation.position, different); 449 } 450 } 451 452 @Override 453 protected void showWaitForInitialization() { 454 super.showWaitForInitialization(); 455 456 FragmentTransaction fragmentTransaction = mActivity.getFragmentManager().beginTransaction(); 457 fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); 458 fragmentTransaction.replace(R.id.conversation_list_pane, getWaitFragment(), TAG_WAIT); 459 fragmentTransaction.commitAllowingStateLoss(); 460 } 461 462 @Override 463 protected void hideWaitForInitialization() { 464 final WaitFragment waitFragment = getWaitFragment(); 465 if (waitFragment == null) { 466 // We aren't showing a wait fragment: nothing to do 467 return; 468 } 469 // Remove the existing wait fragment from the back stack. 470 final FragmentTransaction fragmentTransaction = 471 mActivity.getFragmentManager().beginTransaction(); 472 fragmentTransaction.remove(waitFragment); 473 fragmentTransaction.commitAllowingStateLoss(); 474 super.hideWaitForInitialization(); 475 if (mViewMode.isWaitingForSync()) { 476 // We should come out of wait mode and display the account inbox. 477 loadAccountInbox(); 478 } 479 } 480 481 /** 482 * Up works as follows: 483 * 1) If the user is in a conversation and: 484 * a) the conversation list is hidden (portrait mode), shows the conv list and 485 * stays in conversation view mode. 486 * b) the conversation list is shown, goes back to conversation list mode. 487 * 2) If the user is in search results, up exits search. 488 * mode and returns the user to whatever view they were in when they began search. 489 * 3) If the user is in conversation list mode, there is no up. 490 */ 491 @Override 492 public boolean handleUpPress() { 493 if (isConversationOnlyMode()) { 494 handleBackPress(); 495 } else { 496 toggleDrawerState(); 497 } 498 499 return true; 500 } 501 502 @Override 503 public boolean handleBackPress() { 504 // Clear any visible undo bars. 505 mToastBar.hide(false, false /* actionClicked */); 506 if (isDrawerOpen()) { 507 toggleDrawerState(); 508 } else { 509 popView(false); 510 } 511 return true; 512 } 513 514 /** 515 * Pops the "view stack" to the last screen the user was viewing. 516 * 517 * @param preventClose Whether to prevent closing the app if the stack is empty. 518 */ 519 protected void popView(boolean preventClose) { 520 // If the user is in search query entry mode, or the user is viewing 521 // search results, exit 522 // the mode. 523 int mode = mViewMode.getMode(); 524 if (mode == ViewMode.SEARCH_RESULTS_LIST) { 525 mActivity.finish(); 526 } else if (mode == ViewMode.CONVERSATION || mViewMode.isAdMode()) { 527 // Go to conversation list. 528 mViewMode.enterConversationListMode(); 529 } else if (mode == ViewMode.SEARCH_RESULTS_CONVERSATION) { 530 mViewMode.enterSearchResultsListMode(); 531 } else { 532 // The Folder List fragment can be null for monkeys where we get a back before the 533 // folder list has had a chance to initialize. 534 final FolderListFragment folderList = getFolderListFragment(); 535 if (mode == ViewMode.CONVERSATION_LIST && folderList != null 536 && !Folder.isRoot(mFolder)) { 537 // If the user navigated via the left folders list into a child folder, 538 // back should take the user up to the parent folder's conversation list. 539 navigateUpFolderHierarchy(); 540 // Otherwise, if we are in the conversation list but not in the default 541 // inbox and not on expansive layouts, we want to switch back to the default 542 // inbox. This fixes b/9006969 so that on smaller tablets where we have this 543 // hybrid one and two-pane mode, we will return to the inbox. On larger tablets, 544 // we will instead exit the app. 545 } else if (!preventClose) { 546 // There is nothing else to pop off the stack. 547 mActivity.finish(); 548 } 549 } 550 } 551 552 @Override 553 public boolean shouldShowFirstConversation() { 554 return Intent.ACTION_SEARCH.equals(mActivity.getIntent().getAction()) 555 && shouldEnterSearchConvMode(); 556 } 557 558 @Override 559 public void onUndoAvailable(ToastBarOperation op) { 560 final int mode = mViewMode.getMode(); 561 final ConversationListFragment convList = getConversationListFragment(); 562 563 switch (mode) { 564 case ViewMode.SEARCH_RESULTS_LIST: 565 case ViewMode.CONVERSATION_LIST: 566 case ViewMode.SEARCH_RESULTS_CONVERSATION: 567 case ViewMode.CONVERSATION: 568 if (convList != null) { 569 mToastBar.show(getUndoClickedListener(convList.getAnimatedAdapter()), 570 Utils.convertHtmlToPlainText 571 (op.getDescription(mActivity.getActivityContext())), 572 R.string.undo, 573 true, /* replaceVisibleToast */ 574 op); 575 } 576 } 577 } 578 579 @Override 580 public void onError(final Folder folder, boolean replaceVisibleToast) { 581 showErrorToast(folder, replaceVisibleToast); 582 } 583 584 @Override 585 public boolean isDrawerEnabled() { 586 // two-pane has its own drawer-like thing that expands inline from a minimized state. 587 return false; 588 } 589 590 @Override 591 public int getFolderListViewChoiceMode() { 592 // By default, we want to allow one item to be selected in the folder list 593 return ListView.CHOICE_MODE_SINGLE; 594 } 595 596 private int mMiscellaneousViewTransactionId = -1; 597 598 @Override 599 public void launchFragment(final Fragment fragment, final int selectPosition) { 600 final int containerViewId = TwoPaneLayout.MISCELLANEOUS_VIEW_ID; 601 602 final FragmentManager fragmentManager = mActivity.getFragmentManager(); 603 if (fragmentManager.findFragmentByTag(TAG_CUSTOM_FRAGMENT) == null) { 604 final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); 605 fragmentTransaction.addToBackStack(null); 606 fragmentTransaction.replace(containerViewId, fragment, TAG_CUSTOM_FRAGMENT); 607 mMiscellaneousViewTransactionId = fragmentTransaction.commitAllowingStateLoss(); 608 fragmentManager.executePendingTransactions(); 609 } 610 611 if (selectPosition >= 0) { 612 getConversationListFragment().setRawSelected(selectPosition, true); 613 } 614 } 615 616 @Override 617 public boolean onInterceptCVDownEvent() { 618 // handle a down event on CV by closing the drawer if open 619 if (isDrawerOpen()) { 620 toggleDrawerState(); 621 return true; 622 } 623 return false; 624 } 625 626 @Override 627 public boolean onInterceptKeyFromCV(int keyCode, KeyEvent keyEvent, boolean navigateAway) { 628 // Override left/right key presses in landscape mode. 629 if (navigateAway) { 630 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 631 ConversationListFragment clf = getConversationListFragment(); 632 if (clf != null) { 633 clf.getListView().requestFocus(); 634 } 635 } 636 return true; 637 } 638 return false; 639 } 640 641 @Override 642 public boolean isTwoPaneLandscape() { 643 return mIsTabletLandscape; 644 } 645 646 @Override 647 public boolean shouldShowSearchBarByDefault() { 648 final int mode = mViewMode.getMode(); 649 return mode == ViewMode.SEARCH_RESULTS_LIST || 650 (mIsTabletLandscape && mode == ViewMode.SEARCH_RESULTS_CONVERSATION); 651 } 652 653 @Override 654 public boolean shouldShowSearchMenuItem() { 655 final int mode = mViewMode.getMode(); 656 return mode == ViewMode.CONVERSATION_LIST || 657 (mIsTabletLandscape && mode == ViewMode.CONVERSATION); 658 } 659 660 @Override 661 public void setConversationListLayoutListener( 662 TwoPaneLayout.ConversationListLayoutListener listener) { 663 mConversationListLayoutListener = listener; 664 if (mLayout != null) { 665 mLayout.setConversationListLayoutListener(listener); 666 } 667 } 668} 669