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