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