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