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