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