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