ActionBarController.java revision 80d3875d306c60da83e547c573427627911f8a99
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.ContentUris; 23import android.content.Context; 24import android.content.Loader; 25import android.database.Cursor; 26import android.os.Bundle; 27import android.text.TextUtils; 28import android.util.Log; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.widget.SearchView; 32import android.widget.TextView; 33 34import com.android.email.FolderProperties; 35import com.android.email.R; 36import com.android.email.data.ThrottlingCursorLoader; 37import com.android.emailcommon.Logging; 38import com.android.emailcommon.provider.Account; 39import com.android.emailcommon.provider.EmailContent; 40import com.android.emailcommon.provider.EmailContent.MailboxColumns; 41import com.android.emailcommon.provider.Mailbox; 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 private static final int LOADER_ID_MAILBOX 72 = EmailActivity.ACTION_BAR_CONTROLLER_LOADER_ID_BASE + 1; 73 74 private final Context mContext; 75 private final LoaderManager mLoaderManager; 76 private final ActionBar mActionBar; 77 78 private final View mActionBarCustomView; 79 private final View mMailboxNameContainer; 80 private final TextView mMailboxNameView; 81 private final TextView mUnreadCountView; 82 private final View mSearchContainer; 83 private final SearchView mSearchView; 84 85 private final ActionBarNavigationCallback mActionBarNavigationCallback = 86 new ActionBarNavigationCallback(); 87 88 private final AccountSelectorAdapter mAccountsSelectorAdapter; 89 private AccountSelectorAdapter.CursorWithExtras mAccountCursor; 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 public final Callback mCallback; 100 101 public interface SearchContext { 102 public long getTargetMailboxId(); 103 } 104 105 public interface Callback { 106 /** @return true if an account is selected. */ 107 public boolean isAccountSelected(); 108 109 /** 110 * @return currently selected account ID, {@link Account#ACCOUNT_ID_COMBINED_VIEW}, 111 * or -1 if no account is selected. 112 */ 113 public long getUIAccountId(); 114 115 /** 116 * @return currently selected mailbox ID, or {@link Mailbox#NO_MAILBOX} if no mailbox is 117 * selected. 118 */ 119 public long getMailboxId(); 120 121 /** @return true if the current mailbox name should be shown. */ 122 public boolean shouldShowMailboxName(); 123 124 /** @return the "UP" arrow should be shown. */ 125 public boolean shouldShowUp(); 126 127 /** 128 * Called when an account is selected on the account spinner. 129 * @param accountId ID of the selected account, or {@link Account#ACCOUNT_ID_COMBINED_VIEW}. 130 */ 131 public void onAccountSelected(long accountId); 132 133 /** 134 * Invoked when a recent mailbox is selected on the account spinner. 135 * @param mailboxId The ID of the selected mailbox, or {@link Mailbox#NO_MAILBOX} if the 136 * special option "show all mailboxes" was selected. 137 */ 138 public void onMailboxSelected(long mailboxId); 139 140 /** Called when no accounts are found in the database. */ 141 public void onNoAccountsFound(); 142 143 /** 144 * Called when a search is submitted. 145 * 146 * @param queryTerm query string 147 */ 148 public void onSearchSubmit(String queryTerm); 149 150 /** 151 * Called when the search box is closed. 152 */ 153 public void onSearchExit(); 154 } 155 156 public ActionBarController(Context context, LoaderManager loaderManager, 157 ActionBar actionBar, Callback callback) { 158 mContext = context; 159 mLoaderManager = loaderManager; 160 mActionBar = actionBar; 161 mCallback = callback; 162 mAccountsSelectorAdapter = new AccountSelectorAdapter(mContext); 163 164 mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE 165 | ActionBar.DISPLAY_SHOW_HOME 166 | ActionBar.DISPLAY_SHOW_CUSTOM); 167 168 // Prepare the custom view 169 final LayoutInflater inflater = LayoutInflater.from(mContext); 170 mActionBarCustomView = inflater.inflate(R.layout.action_bar_custom_view, null); 171 final ActionBar.LayoutParams customViewLayout = new ActionBar.LayoutParams( 172 ActionBar.LayoutParams.MATCH_PARENT, 173 ActionBar.LayoutParams.MATCH_PARENT); 174 customViewLayout.setMargins(0 , 0, 0, 0); 175 mActionBar.setCustomView(mActionBarCustomView, customViewLayout); 176 177 // Mailbox name / unread count 178 mMailboxNameContainer = UiUtilities.getView(mActionBarCustomView, 179 R.id.current_mailbox_container); 180 mMailboxNameView = UiUtilities.getView(mMailboxNameContainer, R.id.mailbox_name); 181 mUnreadCountView = UiUtilities.getView(mMailboxNameContainer, R.id.unread_count); 182 183 // Search 184 mSearchContainer = UiUtilities.getView(mActionBarCustomView, R.id.search_container); 185 mSearchView = UiUtilities.getView(mSearchContainer, R.id.search_view); 186 mSearchView.setSubmitButtonEnabled(true); 187 mSearchView.setOnQueryTextListener(mOnQueryText); 188 } 189 190 /** Must be called from {@link UIControllerBase#onActivityCreated()} */ 191 public void onActivityCreated() { 192 loadAccounts(); 193 refresh(); 194 } 195 196 /** Must be called from {@link UIControllerBase#onActivityDestroy()} */ 197 public void onActivityDestroy() { 198 } 199 200 /** Must be called from {@link UIControllerBase#onSaveInstanceState} */ 201 public void onSaveInstanceState(Bundle outState) { 202 outState.putInt(BUNDLE_KEY_MODE, mSearchMode); 203 } 204 205 /** Must be called from {@link UIControllerBase#onRestoreInstanceState} */ 206 public void onRestoreInstanceState(Bundle savedState) { 207 int mode = savedState.getInt(BUNDLE_KEY_MODE); 208 if (mode == MODE_SEARCH) { 209 // No need to re-set the initial query, as the View tree restoration does that 210 enterSearchMode(null); 211 } 212 } 213 214 /** 215 * @return true if the search box is shown. 216 */ 217 private boolean isInSearchMode() { 218 return mSearchMode == MODE_SEARCH; 219 } 220 221 /** 222 * Show the search box. 223 * 224 * @param initialQueryTerm if non-empty, set to the search box. 225 */ 226 public void enterSearchMode(String initialQueryTerm) { 227 if (isInSearchMode()) { 228 return; 229 } 230 if (!TextUtils.isEmpty(initialQueryTerm)) { 231 mSearchView.setQuery(initialQueryTerm, false); 232 } else { 233 mSearchView.setQuery("", false); 234 } 235 mSearchMode = MODE_SEARCH; 236 237 // Need to force it to mode "standard" to hide it. 238 mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); 239 mActionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE); 240 mSearchContainer.setVisibility(View.VISIBLE); 241 242 // Focus on the search input box and throw up the IME if specified. 243 // TODO: HACK. this is a workaround IME not popping up. 244 mSearchView.setIconified(false); 245 246 refresh(); 247 } 248 249 public void exitSearchMode() { 250 if (!isInSearchMode()) { 251 return; 252 } 253 mSearchMode = MODE_NORMAL; 254 mSearchContainer.setVisibility(View.GONE); 255 mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE); 256 257 // Force update of account list when we exit search. 258 updateAccountList(); 259 260 refresh(); 261 mCallback.onSearchExit(); 262 } 263 264 /** 265 * Performs the back action. 266 * 267 * @param isSystemBackKey <code>true</code> if the system back key was pressed. 268 * <code>false</code> if it's caused by the "home" icon click on the action bar. 269 */ 270 public boolean onBackPressed(boolean isSystemBackKey) { 271 if (isInSearchMode()) { 272 exitSearchMode(); 273 return true; 274 } 275 return false; 276 } 277 278 /** Refreshes the action bar display. */ 279 public void refresh() { 280 final boolean showUp = isInSearchMode() || mCallback.shouldShowUp(); 281 mActionBar.setDisplayOptions(showUp 282 ? ActionBar.DISPLAY_HOME_AS_UP : 0, ActionBar.DISPLAY_HOME_AS_UP); 283 284 if (isInSearchMode()) { 285 mMailboxNameContainer.setVisibility(View.GONE); 286 } else { 287 mMailboxNameContainer.setVisibility(mCallback.shouldShowMailboxName() 288 ? View.VISIBLE : View.GONE); 289 } 290 291 // Update the account list only when the account has changed. 292 if (mLastAccountIdForDirtyCheck != mCallback.getUIAccountId()) { 293 mLastAccountIdForDirtyCheck = mCallback.getUIAccountId(); 294 // If the selected account changes, reload the cursor to update the recent mailboxes 295 if (mLastAccountIdForDirtyCheck != Account.NO_ACCOUNT) { 296 mLoaderManager.destroyLoader(LOADER_ID_ACCOUNT_LIST); 297 loadAccounts(); 298 } else { 299 updateAccountList(); 300 } 301 } 302 303 // Update current mailbox info 304 final long mailboxId = mCallback.getMailboxId(); 305 if (mailboxId == Mailbox.NO_MAILBOX) { 306 clearMailboxInfo(); 307 } else { 308 if (mLastMailboxIdForDirtyCheck != mailboxId) { 309 mLastMailboxIdForDirtyCheck = mailboxId; 310 loadMailboxInfo(mailboxId); 311 } 312 } 313 } 314 315 /** 316 * Load account cursor, and update the action bar. 317 */ 318 private void loadAccounts() { 319 mLoaderManager.initLoader(LOADER_ID_ACCOUNT_LIST, null, 320 new LoaderCallbacks<Cursor>() { 321 @Override 322 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 323 return AccountSelectorAdapter.createLoader(mContext, mCallback.getUIAccountId()); 324 } 325 326 @Override 327 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 328 mAccountCursor = (AccountSelectorAdapter.CursorWithExtras) data; 329 updateAccountList(); 330 } 331 332 @Override 333 public void onLoaderReset(Loader<Cursor> loader) { 334 mAccountCursor = null; 335 updateAccountList(); 336 } 337 }); 338 } 339 340 /** 341 * Called when the LOADER_ID_ACCOUNT_LIST loader loads the data. Update the account spinner 342 * on the action bar. 343 */ 344 private void updateAccountList() { 345 mAccountsSelectorAdapter.swapCursor(mAccountCursor); 346 347 if (mSearchMode == MODE_SEARCH) { 348 // In search mode, so we don't care about the account list - it'll get updated when 349 // it goes visible again. 350 return; 351 } 352 353 final ActionBar ab = mActionBar; 354 if (mAccountCursor == null) { 355 // Cursor not ready or closed. 356 ab.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE); 357 ab.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); 358 return; 359 } 360 361 final int count = mAccountCursor.getAccountCount() + mAccountCursor.getRecentMailboxCount(); 362 if (count == 0) { 363 mCallback.onNoAccountsFound(); 364 return; 365 } 366 367 // If only one account, don't show the drop down. 368 int selectedPosition = mAccountCursor.getPosition(mCallback.getUIAccountId()); 369 if (count == 1) { 370 // Show the account name as the title. 371 ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE); 372 ab.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); 373 if (selectedPosition >= 0) { 374 mAccountCursor.moveToPosition(selectedPosition); 375 ab.setTitle(AccountSelectorAdapter.getDisplayName(mAccountCursor)); 376 } 377 return; 378 } 379 380 // Update the drop down list. 381 if (ab.getNavigationMode() != ActionBar.NAVIGATION_MODE_LIST) { 382 ab.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE); 383 ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); 384 ab.setListNavigationCallbacks(mAccountsSelectorAdapter, mActionBarNavigationCallback); 385 } 386 // Find the currently selected account, and select it. 387 if (selectedPosition >= 0) { 388 ab.setSelectedNavigationItem(selectedPosition); 389 } 390 } 391 392 private class ActionBarNavigationCallback implements ActionBar.OnNavigationListener { 393 @Override 394 public boolean onNavigationItemSelected(int itemPosition, long itemId) { 395 if (mAccountsSelectorAdapter.isAccountItem(itemPosition) 396 && itemId != mCallback.getUIAccountId()) { 397 mCallback.onAccountSelected(itemId); 398 } else if (mAccountsSelectorAdapter.isMailboxItem(itemPosition)) { 399 mCallback.onMailboxSelected(itemId); 400 // We need to update the selection, otherwise the user is unable to select the 401 // recent folder a second time w/o first selecting another item in the spinner 402 int selectedPosition = mAccountsSelectorAdapter.getAccountPosition(itemPosition); 403 if (selectedPosition != AccountSelectorAdapter.UNKNOWN_POSITION) { 404 mActionBar.setSelectedNavigationItem(selectedPosition); 405 } 406 } else { 407 Log.i(Logging.LOG_TAG, 408 "Invalid type selected in ActionBarController at index " + itemPosition); 409 } 410 return true; 411 } 412 } 413 414 private static final String[] MAILBOX_NAME_COUNT_PROJECTION = new String[] { 415 MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, 416 MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT 417 }; 418 419 private void loadMailboxInfo(final long mailboxId) { 420 clearMailboxInfo(); 421 mLoaderManager.restartLoader(LOADER_ID_MAILBOX, null, 422 new LoaderCallbacks<Cursor>() { 423 @Override 424 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 425 return new ThrottlingCursorLoader(mContext, 426 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 427 MAILBOX_NAME_COUNT_PROJECTION, null, null, null); 428 } 429 430 @Override 431 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 432 // Update action bar 433 FolderProperties fp = FolderProperties.getInstance(mContext); 434 updateMailboxInfo( 435 fp.getDisplayName(cursor), 436 fp.getMessageCount(cursor) 437 ); 438 } 439 440 @Override 441 public void onLoaderReset(Loader<Cursor> loader) { 442 } 443 }); 444 } 445 446 private void clearMailboxInfo() { 447 updateMailboxInfo("", 0); 448 } 449 450 private void updateMailboxInfo(String mailboxName, int count) { 451 mMailboxNameView.setText(mailboxName); 452 mUnreadCountView.setText(UiUtilities.getMessageCountForUi(mContext, count, true)); 453 } 454 455 private final SearchView.OnQueryTextListener mOnQueryText 456 = new SearchView.OnQueryTextListener() { 457 @Override 458 public boolean onQueryTextChange(String newText) { 459 // Event not handled. Let the search do the default action. 460 return false; 461 } 462 463 @Override 464 public boolean onQueryTextSubmit(String query) { 465 mCallback.onSearchSubmit(mSearchView.getQuery().toString()); 466 return true; // Event handled. 467 } 468 }; 469 470} 471