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 17 package com.android.email.activity; 18 19 import android.app.ActionBar; 20 import android.app.LoaderManager; 21 import android.app.LoaderManager.LoaderCallbacks; 22 import android.content.Context; 23 import android.content.Loader; 24 import android.database.Cursor; 25 import android.graphics.drawable.Drawable; 26 import android.os.Bundle; 27 import android.text.SpannableString; 28 import android.text.SpannableStringBuilder; 29 import android.text.TextUtils; 30 import android.text.style.TextAppearanceSpan; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.AdapterView; 35 import android.widget.AdapterView.OnItemClickListener; 36 import android.widget.ListPopupWindow; 37 import android.widget.ListView; 38 import android.widget.SearchView; 39 import android.widget.TextView; 40 41 import com.android.email.R; 42 import com.android.emailcommon.provider.Account; 43 import com.android.emailcommon.provider.Mailbox; 44 import com.android.emailcommon.utility.DelayedOperations; 45 import com.android.emailcommon.utility.Utility; 46 47 /** 48 * Manages the account name and the custom view part on the action bar. 49 */ 50 public class ActionBarController { 51 private static final String BUNDLE_KEY_MODE = "ActionBarController.BUNDLE_KEY_MODE"; 52 53 /** 54 * Constants for {@link #mSearchMode}. 55 * 56 * In {@link #MODE_NORMAL} mode, we don't show the search box. 57 * In {@link #MODE_SEARCH} mode, we do show the search box. 58 * The action bar doesn't really care if the activity is showing search results. 59 * If the activity is showing search results, and the {@link Callback#onSearchExit} is called, 60 * the activity probably wants to close itself, but this class doesn't make the desision. 61 */ 62 private static final int MODE_NORMAL = 0; 63 private static final int MODE_SEARCH = 1; 64 65 private static final int LOADER_ID_ACCOUNT_LIST 66 = EmailActivity.ACTION_BAR_CONTROLLER_LOADER_ID_BASE + 0; 67 68 private static final int DISPLAY_TITLE_MULTIPLE_LINES = 0x20; 69 private static final int ACTION_BAR_MASK = 70 ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_CUSTOM 71 | ActionBar.DISPLAY_SHOW_TITLE | DISPLAY_TITLE_MULTIPLE_LINES; 72 private static final int CUSTOM_ACTION_BAR_OPTIONS = 73 ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_CUSTOM; 74 75 private final Context mContext; 76 private final LoaderManager mLoaderManager; 77 private final ActionBar mActionBar; 78 private final DelayedOperations mDelayedOperations; 79 80 /** "Folders" label shown with account name on 1-pane mailbox list */ 81 private final String mAllFoldersLabel; 82 83 private final ViewGroup mActionBarCustomView; 84 private final ViewGroup mAccountSpinnerContainer; 85 private final View mAccountSpinner; 86 private final Drawable mAccountSpinnerDefaultBackground; 87 private final TextView mAccountSpinnerLine1View; 88 private final TextView mAccountSpinnerLine2View; 89 private final TextView mAccountSpinnerCountView; 90 91 private View mSearchContainer; 92 private SearchView mSearchView; 93 94 private final AccountDropdownPopup mAccountDropdown; 95 96 private final AccountSelectorAdapter mAccountsSelectorAdapter; 97 98 private AccountSelectorAdapter.CursorWithExtras mCursor; 99 100 /** The current account ID; used to determine if the account has changed. */ 101 private long mLastAccountIdForDirtyCheck = Account.NO_ACCOUNT; 102 103 /** The current mailbox ID; used to determine if the mailbox has changed. */ 104 private long mLastMailboxIdForDirtyCheck = Mailbox.NO_MAILBOX; 105 106 /** Either {@link #MODE_NORMAL} or {@link #MODE_SEARCH}. */ 107 private int mSearchMode = MODE_NORMAL; 108 109 /** The current title mode, which should be one of {@code Callback TITLE_MODE_*} */ 110 private int mTitleMode; 111 112 public final Callback mCallback; 113 114 public interface SearchContext { getTargetMailboxId()115 public long getTargetMailboxId(); 116 } 117 118 private static final int TITLE_MODE_SPINNER_ENABLED = 0x10; 119 120 public interface Callback { 121 /** Values for {@link #getTitleMode}. Show only account name */ 122 public static final int TITLE_MODE_ACCOUNT_NAME_ONLY = 0 | TITLE_MODE_SPINNER_ENABLED; 123 124 /** 125 * Show the current account name with "Folders" 126 * The account spinner will be disabled in this mode. 127 */ 128 public static final int TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL = 1; 129 130 /** 131 * Show the current account name and the current mailbox name. 132 */ 133 public static final int TITLE_MODE_ACCOUNT_WITH_MAILBOX = 2 | TITLE_MODE_SPINNER_ENABLED; 134 /** 135 * Show the current message subject. Actual subject is obtained via 136 * {@link #getMessageSubject()}. 137 * 138 * The account spinner will be disabled in this mode. 139 */ 140 public static final int TITLE_MODE_MESSAGE_SUBJECT = 3; 141 142 /** @return true if an account is selected. */ isAccountSelected()143 public boolean isAccountSelected(); 144 145 /** 146 * @return currently selected account ID, {@link Account#ACCOUNT_ID_COMBINED_VIEW}, 147 * or -1 if no account is selected. 148 */ getUIAccountId()149 public long getUIAccountId(); 150 151 /** 152 * @return currently selected mailbox ID, or {@link Mailbox#NO_MAILBOX} if no mailbox is 153 * selected. 154 */ getMailboxId()155 public long getMailboxId(); 156 157 /** 158 * @return constants such as {@link #TITLE_MODE_ACCOUNT_NAME_ONLY}. 159 */ getTitleMode()160 public int getTitleMode(); 161 162 /** @see #TITLE_MODE_MESSAGE_SUBJECT */ getMessageSubject()163 public String getMessageSubject(); 164 165 /** @return the "UP" arrow should be shown. */ shouldShowUp()166 public boolean shouldShowUp(); 167 168 /** 169 * Called when an account is selected on the account spinner. 170 * @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 171 */ onAccountSelected(long accountId)172 public void onAccountSelected(long accountId); 173 174 /** 175 * Invoked when a recent mailbox is selected on the account spinner. 176 * 177 * @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 178 * @param mailboxId The ID of the selected mailbox, or {@link Mailbox#NO_MAILBOX} if the 179 * special option "show all mailboxes" was selected. 180 */ onMailboxSelected(long accountId, long mailboxId)181 public void onMailboxSelected(long accountId, long mailboxId); 182 183 /** Called when no accounts are found in the database. */ onNoAccountsFound()184 public void onNoAccountsFound(); 185 186 /** 187 * Retrieves the hint text to be shown for when a search entry is being made. 188 */ getSearchHint()189 public String getSearchHint(); 190 191 /** 192 * Called when the action bar initially shows the search entry field. 193 */ onSearchStarted()194 public void onSearchStarted(); 195 196 /** 197 * Called when a search is submitted. 198 * 199 * @param queryTerm query string 200 */ onSearchSubmit(String queryTerm)201 public void onSearchSubmit(String queryTerm); 202 203 /** 204 * Called when the search box is closed. 205 */ onSearchExit()206 public void onSearchExit(); 207 onUpPressed()208 public void onUpPressed(); 209 } 210 ActionBarController(Context context, LoaderManager loaderManager, ActionBar actionBar, Callback callback)211 public ActionBarController(Context context, LoaderManager loaderManager, 212 ActionBar actionBar, Callback callback) { 213 mContext = context; 214 mLoaderManager = loaderManager; 215 mActionBar = actionBar; 216 mCallback = callback; 217 mDelayedOperations = new DelayedOperations(Utility.getMainThreadHandler()); 218 mAllFoldersLabel = mContext.getResources().getString( 219 R.string.action_bar_mailbox_list_title); 220 mAccountsSelectorAdapter = new AccountSelectorAdapter(mContext); 221 222 // Configure action bar. 223 enterCustomActionBarMode(); 224 225 // Prepare the custom view 226 mActionBar.setCustomView(R.layout.action_bar_custom_view); 227 mActionBarCustomView = (ViewGroup) mActionBar.getCustomView(); 228 229 // Account spinner 230 mAccountSpinnerContainer = 231 UiUtilities.getView(mActionBarCustomView, R.id.account_spinner_container); 232 mAccountSpinner = UiUtilities.getView(mActionBarCustomView, R.id.account_spinner); 233 mAccountSpinnerDefaultBackground = mAccountSpinner.getBackground(); 234 235 mAccountSpinnerLine1View = UiUtilities.getView(mActionBarCustomView, R.id.spinner_line_1); 236 mAccountSpinnerLine2View = UiUtilities.getView(mActionBarCustomView, R.id.spinner_line_2); 237 mAccountSpinnerCountView = UiUtilities.getView(mActionBarCustomView, R.id.spinner_count); 238 239 // Account dropdown 240 mAccountDropdown = new AccountDropdownPopup(mContext); 241 mAccountDropdown.setAdapter(mAccountsSelectorAdapter); 242 243 mAccountSpinner.setOnClickListener(new View.OnClickListener() { 244 @Override public void onClick(View v) { 245 if (mAccountsSelectorAdapter.getCount() > 0) { 246 mAccountDropdown.show(); 247 } 248 } 249 }); 250 // this other click listener handles clicks that ought to be aliased to "up" 251 // only one or the other listener should be active at any time 252 mActionBarCustomView.setOnClickListener(new View.OnClickListener() { 253 @Override 254 public void onClick(View v) { 255 mCallback.onUpPressed(); 256 } 257 }); 258 // pick a sane default. later enabled in updateTitle(). 259 mActionBarCustomView.setClickable(false); 260 } 261 initSearchViews()262 private void initSearchViews() { 263 if (mSearchContainer == null) { 264 final LayoutInflater inflater = LayoutInflater.from(mContext); 265 mSearchContainer = inflater.inflate(R.layout.action_bar_search, null); 266 mSearchView = UiUtilities.getView(mSearchContainer, R.id.search_view); 267 mSearchView.setSubmitButtonEnabled(false); 268 mSearchView.setOnQueryTextListener(mOnQueryText); 269 mSearchView.onActionViewExpanded(); 270 mActionBarCustomView.addView(mSearchContainer); 271 } 272 } 273 274 275 /** Must be called from {@link UIControllerBase#onActivityCreated()} */ onActivityCreated()276 public void onActivityCreated() { 277 refresh(); 278 } 279 280 /** Must be called from {@link UIControllerBase#onActivityDestroy()} */ onActivityDestroy()281 public void onActivityDestroy() { 282 if (mAccountDropdown.isShowing()) { 283 mAccountDropdown.dismiss(); 284 } 285 } 286 287 /** Must be called from {@link UIControllerBase#onSaveInstanceState} */ onSaveInstanceState(Bundle outState)288 public void onSaveInstanceState(Bundle outState) { 289 mDelayedOperations.removeCallbacks(); // Remove all pending operations 290 outState.putInt(BUNDLE_KEY_MODE, mSearchMode); 291 } 292 293 /** Must be called from {@link UIControllerBase#onRestoreInstanceState} */ onRestoreInstanceState(Bundle savedState)294 public void onRestoreInstanceState(Bundle savedState) { 295 int mode = savedState.getInt(BUNDLE_KEY_MODE); 296 if (mode == MODE_SEARCH) { 297 // No need to re-set the initial query, as the View tree restoration does that 298 enterSearchMode(null); 299 } 300 } 301 302 /** 303 * @return true if the search box is shown. 304 */ isInSearchMode()305 public boolean isInSearchMode() { 306 return mSearchMode == MODE_SEARCH; 307 } 308 309 /** 310 * @return Whether or not the search bar should be shown. This is a function of whether or not a 311 * search is active, and if the current layout supports it. 312 */ shouldShowSearchBar()313 private boolean shouldShowSearchBar() { 314 return isInSearchMode() && (mTitleMode != Callback.TITLE_MODE_MESSAGE_SUBJECT); 315 } 316 317 /** 318 * Show the search box. 319 * 320 * @param initialQueryTerm if non-empty, set to the search box. 321 */ enterSearchMode(String initialQueryTerm)322 public void enterSearchMode(String initialQueryTerm) { 323 initSearchViews(); 324 if (isInSearchMode()) { 325 return; 326 } 327 if (!TextUtils.isEmpty(initialQueryTerm)) { 328 mSearchView.setQuery(initialQueryTerm, false); 329 } else { 330 mSearchView.setQuery("", false); 331 } 332 mSearchView.setQueryHint(mCallback.getSearchHint()); 333 334 mSearchMode = MODE_SEARCH; 335 336 // Focus on the search input box and throw up the IME if specified. 337 // TODO: HACK. this is a workaround IME not popping up. 338 mSearchView.setIconified(false); 339 340 refresh(); 341 mCallback.onSearchStarted(); 342 } 343 exitSearchMode()344 public void exitSearchMode() { 345 if (!isInSearchMode()) { 346 return; 347 } 348 mSearchMode = MODE_NORMAL; 349 350 refresh(); 351 mCallback.onSearchExit(); 352 } 353 354 /** 355 * Performs the back action. 356 * 357 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 358 * <code>false</code> if it's caused by the "home" icon click on the action bar. 359 */ onBackPressed(boolean isSystemBackKey)360 public boolean onBackPressed(boolean isSystemBackKey) { 361 if (shouldShowSearchBar()) { 362 exitSearchMode(); 363 return true; 364 } 365 return false; 366 } 367 368 /** Refreshes the action bar display. */ refresh()369 public void refresh() { 370 // The actual work is in refreshInernal(), but we don't call it directly here, because: 371 // 1. refresh() is called very often. 372 // 2. to avoid nested fragment transaction. 373 // refresh is often called during a fragment transaction, but updateTitle() may call 374 // a callback which would initiate another fragment transaction. 375 mDelayedOperations.removeCallbacks(mRefreshRunnable); 376 mDelayedOperations.post(mRefreshRunnable); 377 } 378 379 private final Runnable mRefreshRunnable = new Runnable() { 380 @Override public void run() { 381 refreshInernal(); 382 } 383 }; refreshInernal()384 private void refreshInernal() { 385 final boolean showUp = isInSearchMode() || mCallback.shouldShowUp(); 386 mActionBar.setDisplayOptions(showUp 387 ? ActionBar.DISPLAY_HOME_AS_UP : 0, ActionBar.DISPLAY_HOME_AS_UP); 388 389 final long accountId = mCallback.getUIAccountId(); 390 final long mailboxId = mCallback.getMailboxId(); 391 if ((mLastAccountIdForDirtyCheck != accountId) 392 || (mLastMailboxIdForDirtyCheck != mailboxId)) { 393 mLastAccountIdForDirtyCheck = accountId; 394 mLastMailboxIdForDirtyCheck = mailboxId; 395 396 if (accountId != Account.NO_ACCOUNT) { 397 loadAccountMailboxInfo(accountId, mailboxId); 398 } 399 } 400 401 updateTitle(); 402 } 403 404 /** 405 * Load account/mailbox info, and account/recent mailbox list. 406 */ loadAccountMailboxInfo(final long accountId, final long mailboxId)407 private void loadAccountMailboxInfo(final long accountId, final long mailboxId) { 408 mLoaderManager.restartLoader(LOADER_ID_ACCOUNT_LIST, null, 409 new LoaderCallbacks<Cursor>() { 410 @Override 411 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 412 return AccountSelectorAdapter.createLoader(mContext, accountId, mailboxId); 413 } 414 415 @Override 416 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 417 mCursor = (AccountSelectorAdapter.CursorWithExtras) data; 418 updateTitle(); 419 } 420 421 @Override 422 public void onLoaderReset(Loader<Cursor> loader) { 423 mCursor = null; 424 updateTitle(); 425 } 426 }); 427 } 428 429 /** 430 * Update the "title" part. 431 */ updateTitle()432 private void updateTitle() { 433 mAccountsSelectorAdapter.swapCursor(mCursor); 434 435 enterCustomActionBarMode(); 436 if (mCursor == null) { 437 // Initial load not finished. 438 mActionBarCustomView.setVisibility(View.GONE); 439 return; 440 } 441 mActionBarCustomView.setVisibility(View.VISIBLE); 442 443 if (mCursor.getAccountCount() == 0) { 444 mCallback.onNoAccountsFound(); 445 return; 446 } 447 448 if ((mCursor.getAccountId() != Account.NO_ACCOUNT) && !mCursor.accountExists()) { 449 // Account specified, but does not exist. 450 if (isInSearchMode()) { 451 exitSearchMode(); 452 } 453 454 // Switch to the default account. 455 mCallback.onAccountSelected(Account.getDefaultAccountId(mContext)); 456 return; 457 } 458 459 mTitleMode = mCallback.getTitleMode(); 460 461 if (shouldShowSearchBar()) { 462 initSearchViews(); 463 // In search mode, the search box is a replacement of the account spinner, so ignore 464 // the work needed to update that. It will get updated when it goes visible again. 465 mAccountSpinnerContainer.setVisibility(View.GONE); 466 mSearchContainer.setVisibility(View.VISIBLE); 467 return; 468 } 469 470 // Account spinner visible. 471 mAccountSpinnerContainer.setVisibility(View.VISIBLE); 472 UiUtilities.setVisibilitySafe(mSearchContainer, View.GONE); 473 474 if (mTitleMode == Callback.TITLE_MODE_MESSAGE_SUBJECT) { 475 // Use two line title action bar mode 476 enterMultiLineTitleActionBarMode(); 477 String subject = mCallback.getMessageSubject(); 478 if (subject == null) { 479 subject = ""; 480 } 481 final SpannableString title = new SpannableString(subject); 482 final SpannableStringBuilder builder = new SpannableStringBuilder(); 483 title.setSpan(new TextAppearanceSpan(mContext, R.style.subject_action_bar_title_text), 484 0, subject.length(), 0); 485 builder.append(title); 486 mActionBar.setTitle(builder); 487 mActionBar.setSubtitle(null); 488 } else if (mTitleMode == Callback.TITLE_MODE_ACCOUNT_WITH_ALL_FOLDERS_LABEL) { 489 enterSingleLineTitleActionBarMode(); 490 mActionBar.setTitle(mAllFoldersLabel); 491 mActionBar.setSubtitle(mCursor.getAccountDisplayName()); 492 } else { 493 // Get mailbox name 494 final String mailboxName; 495 if (mTitleMode == Callback.TITLE_MODE_ACCOUNT_WITH_MAILBOX) { 496 mailboxName = mCursor.getMailboxDisplayName(); 497 } else { 498 mailboxName = null; 499 } 500 501 // Note - setSingleLine is needed as well as setMaxLines since they set different 502 // flags on the view. 503 mAccountSpinnerLine1View.setSingleLine(); 504 mAccountSpinnerLine1View.setMaxLines(1); 505 if (TextUtils.isEmpty(mailboxName)) { 506 mAccountSpinnerLine1View.setText(mCursor.getAccountDisplayName()); 507 508 // Change the visibility of line 2, so line 1 will be vertically-centered. 509 mAccountSpinnerLine2View.setVisibility(View.GONE); 510 } else { 511 mAccountSpinnerLine1View.setText(mailboxName); 512 mAccountSpinnerLine2View.setVisibility(View.VISIBLE); 513 mAccountSpinnerLine2View.setText(mCursor.getAccountDisplayName()); 514 } 515 516 mAccountSpinnerCountView.setVisibility(View.VISIBLE); 517 mAccountSpinnerCountView.setText(UiUtilities.getMessageCountForUi( 518 mContext, mCursor.getMailboxMessageCount(), true)); 519 } 520 521 boolean spinnerEnabled = 522 ((mTitleMode & TITLE_MODE_SPINNER_ENABLED) != 0) && mCursor.shouldEnableSpinner(); 523 524 setSpinnerEnabled(spinnerEnabled); 525 } 526 enterCustomActionBarMode()527 private void enterCustomActionBarMode() { 528 mActionBar.setDisplayOptions(CUSTOM_ACTION_BAR_OPTIONS, ACTION_BAR_MASK); 529 } 530 enterMultiLineTitleActionBarMode()531 private void enterMultiLineTitleActionBarMode() { 532 mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE 533 | DISPLAY_TITLE_MULTIPLE_LINES, ACTION_BAR_MASK); 534 } 535 enterSingleLineTitleActionBarMode()536 private void enterSingleLineTitleActionBarMode() { 537 mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE, 538 ACTION_BAR_MASK); 539 } 540 setSpinnerEnabled(boolean enabled)541 private void setSpinnerEnabled(boolean enabled) { 542 if (enabled == mAccountSpinner.isEnabled()) { 543 return; 544 } 545 546 mAccountSpinner.setEnabled(enabled); 547 mAccountSpinner.setClickable(enabled); 548 mActionBarCustomView.setClickable(!enabled); 549 if (enabled) { 550 mAccountSpinner.setBackgroundDrawable(mAccountSpinnerDefaultBackground); 551 } else { 552 mAccountSpinner.setBackgroundDrawable(null); 553 } 554 555 // For some reason, changing the background mucks with the padding so we have to manually 556 // reset vertical padding here (also specified in XML, but it seems to be ignored for 557 // some reason. 558 mAccountSpinner.setPadding( 559 mAccountSpinner.getPaddingLeft(), 560 0, 561 mAccountSpinner.getPaddingRight(), 562 0); 563 } 564 565 566 private final SearchView.OnQueryTextListener mOnQueryText 567 = new SearchView.OnQueryTextListener() { 568 @Override 569 public boolean onQueryTextChange(String newText) { 570 // Event not handled. Let the search do the default action. 571 return false; 572 } 573 574 @Override 575 public boolean onQueryTextSubmit(String query) { 576 mCallback.onSearchSubmit(mSearchView.getQuery().toString()); 577 return true; // Event handled. 578 } 579 }; 580 onAccountSpinnerItemClicked(int position)581 private void onAccountSpinnerItemClicked(int position) { 582 if (mAccountsSelectorAdapter == null) { // just in case... 583 return; 584 } 585 final long accountId = mAccountsSelectorAdapter.getAccountId(position); 586 587 if (mAccountsSelectorAdapter.isAccountItem(position)) { 588 mCallback.onAccountSelected(accountId); 589 } else if (mAccountsSelectorAdapter.isMailboxItem(position)) { 590 mCallback.onMailboxSelected(accountId, 591 mAccountsSelectorAdapter.getId(position)); 592 } 593 } 594 595 // Based on Spinner.DropdownPopup 596 private class AccountDropdownPopup extends ListPopupWindow { AccountDropdownPopup(Context context)597 public AccountDropdownPopup(Context context) { 598 super(context); 599 setAnchorView(mAccountSpinner); 600 setModal(true); 601 setPromptPosition(POSITION_PROMPT_ABOVE); 602 setOnItemClickListener(new OnItemClickListener() { 603 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 604 onAccountSpinnerItemClicked(position); 605 dismiss(); 606 } 607 }); 608 } 609 610 @Override show()611 public void show() { 612 setWidth(mContext.getResources().getDimensionPixelSize( 613 R.dimen.account_dropdown_dropdownwidth)); 614 setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 615 super.show(); 616 // List view is instantiated in super.show(), so we need to do this after... 617 getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); 618 } 619 } 620 } 621