AccountSelectorAdapter.java revision 22cc33158d1336fe39b44046bc584c0f1c4838f5
1/* 2 * Copyright (C) 2010 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.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.Context; 22import android.content.Loader; 23import android.database.Cursor; 24import android.database.MatrixCursor; 25import android.view.LayoutInflater; 26import android.view.View; 27import android.view.ViewGroup; 28import android.widget.AdapterView; 29import android.widget.CursorAdapter; 30import android.widget.TextView; 31 32import com.android.email.FolderProperties; 33import com.android.email.R; 34import com.android.email.ResourceHelper; 35import com.android.email.data.ClosingMatrixCursor; 36import com.android.email.data.ThrottlingCursorLoader; 37import com.android.emailcommon.provider.Account; 38import com.android.emailcommon.provider.EmailContent; 39import com.android.emailcommon.provider.EmailContent.AccountColumns; 40import com.android.emailcommon.provider.EmailContent.MailboxColumns; 41import com.android.emailcommon.provider.Mailbox; 42import com.android.emailcommon.utility.Utility; 43import com.google.common.annotations.VisibleForTesting; 44import com.google.common.base.Preconditions; 45 46import java.util.ArrayList; 47import java.util.Collection; 48 49/** 50 * Account selector spinner. 51 * 52 * TODO Test it! 53 */ 54public class AccountSelectorAdapter extends CursorAdapter { 55 /** meta data column for an message count (unread or total, depending on row) */ 56 private static final String MESSAGE_COUNT = "unreadCount"; 57 58 /** meta data column for the row type; used for display purposes */ 59 private static final String ROW_TYPE = "rowType"; 60 61 /** meta data position of the currently selected account in the drop-down list */ 62 private static final String ACCOUNT_POSITION = "accountPosition"; 63 64 /** "account id" virtual column name for the matrix cursor */ 65 private static final String ACCOUNT_ID = "accountId"; 66 67 private static final int ROW_TYPE_HEADER = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 68 @SuppressWarnings("unused") 69 private static final int ROW_TYPE_MAILBOX = 0; 70 private static final int ROW_TYPE_ACCOUNT = 1; 71 private static final int ITEM_VIEW_TYPE_ACCOUNT = 0; 72 static final int UNKNOWN_POSITION = -1; 73 /** Projection for account database query */ 74 private static final String[] ACCOUNT_PROJECTION = new String[] { 75 EmailContent.RECORD_ID, 76 Account.DISPLAY_NAME, 77 Account.EMAIL_ADDRESS, 78 }; 79 /** 80 * Projection used for the selector display; we add meta data that doesn't exist in the 81 * account database, so, this should be a super-set of {@link #ACCOUNT_PROJECTION}. 82 */ 83 private static final String[] ADAPTER_PROJECTION = new String[] { 84 ROW_TYPE, 85 EmailContent.RECORD_ID, 86 Account.DISPLAY_NAME, 87 Account.EMAIL_ADDRESS, 88 MESSAGE_COUNT, 89 ACCOUNT_POSITION, // TODO Probably we don't really need this 90 ACCOUNT_ID, 91 }; 92 93 /** Sort order. Show the default account first. */ 94 private static final String ORDER_BY = Account.IS_DEFAULT + " desc, " + Account.RECORD_ID; 95 96 @SuppressWarnings("hiding") 97 private final Context mContext; 98 private final LayoutInflater mInflater; 99 private final ResourceHelper mResourceHelper; 100 101 /** 102 * Returns a loader that can populate the account spinner. 103 * @param context a context 104 * @param accountId the ID of the currently viewed account 105 */ 106 public static Loader<Cursor> createLoader(Context context, long accountId, long mailboxId) { 107 return new AccountsLoader(context, accountId, mailboxId, UiUtilities.useTwoPane(context)); 108 } 109 110 public AccountSelectorAdapter(Context context) { 111 super(context, null, 0 /* no auto-requery */); 112 mContext = context; 113 mInflater = LayoutInflater.from(context); 114 mResourceHelper = ResourceHelper.getInstance(context); 115 } 116 117 /** 118 * {@inheritDoc} 119 * 120 * The account selector view can contain one of four types of row data: 121 * <ol> 122 * <li>headers</li> 123 * <li>accounts</li> 124 * <li>recent mailboxes</li> 125 * <li>"show all folders"</li> 126 * </ol> 127 * Headers are handled separately as they have a unique layout and cannot be interacted with. 128 * Accounts, recent mailboxes and "show all folders" all have the same interaction model and 129 * share a very similar layout. The single difference is that both accounts and recent 130 * mailboxes display an unread count; whereas "show all folders" does not. To determine 131 * if a particular row is "show all folders" verify that a) it's not an account row and 132 * b) it's ID is {@link Mailbox#NO_MAILBOX}. 133 * 134 * TODO Use recycled views. ({@link #getViewTypeCount} and {@link #getItemViewType}) 135 */ 136 @Override 137 public View getView(int position, View convertView, ViewGroup parent) { 138 Cursor c = getCursor(); 139 c.moveToPosition(position); 140 View view; 141 if (c.getInt(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER) { 142 view = mInflater.inflate(R.layout.action_bar_spinner_dropdown_header, parent, false); 143 final TextView displayNameView = (TextView) view.findViewById(R.id.display_name); 144 final String displayName = getDisplayName(c); 145 displayNameView.setText(displayName); 146 } else { 147 view = mInflater.inflate(R.layout.action_bar_spinner_dropdown, parent, false); 148 final TextView displayNameView = (TextView) view.findViewById(R.id.display_name); 149 final TextView emailAddressView = (TextView) view.findViewById(R.id.email_address); 150 final TextView unreadCountView = (TextView) view.findViewById(R.id.unread_count); 151 final View chipView = view.findViewById(R.id.color_chip); 152 153 final String displayName = getDisplayName(c); 154 final String emailAddress = getAccountEmailAddress(c); 155 156 displayNameView.setText(displayName); 157 158 // Show the email address only when it's different from the display name. 159 if (displayName.equals(emailAddress)) { 160 emailAddressView.setVisibility(View.GONE); 161 } else { 162 emailAddressView.setVisibility(View.VISIBLE); 163 emailAddressView.setText(emailAddress); 164 } 165 166 boolean isAccount = isAccountItem(c); 167 long id = getId(c); 168 if (isAccount || id != Mailbox.NO_MAILBOX) { 169 unreadCountView.setVisibility(View.VISIBLE); 170 unreadCountView.setText(UiUtilities.getMessageCountForUi(mContext, 171 getAccountUnreadCount(c), true)); 172 173 if (((CursorWithExtras) c).getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) { 174 chipView.setBackgroundColor(mResourceHelper.getAccountColor(id)); 175 chipView.setVisibility(View.VISIBLE); 176 } else { 177 chipView.setVisibility(View.GONE); 178 } 179 } else { 180 unreadCountView.setVisibility(View.INVISIBLE); 181 chipView.setVisibility(View.GONE); 182 } 183 184 } 185 return view; 186 } 187 188 @Override 189 public View newView(Context context, Cursor cursor, ViewGroup parent) { 190 return null; // we don't reuse views. This method never gets called. 191 } 192 193 @Override 194 public void bindView(View view, Context context, Cursor cursor) { 195 // we don't reuse views. This method never gets called. 196 } 197 198 @Override 199 public int getViewTypeCount() { 200 return 2; 201 } 202 203 @Override 204 public int getItemViewType(int position) { 205 Cursor c = getCursor(); 206 c.moveToPosition(position); 207 return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER 208 ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER 209 : ITEM_VIEW_TYPE_ACCOUNT; 210 } 211 212 @Override 213 public boolean isEnabled(int position) { 214 return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER); 215 } 216 217 public boolean isAccountItem(int position) { 218 Cursor c = getCursor(); 219 c.moveToPosition(position); 220 return isAccountItem(c); 221 } 222 223 public boolean isAccountItem(Cursor c) { 224 return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_ACCOUNT); 225 } 226 227 public boolean isMailboxItem(int position) { 228 Cursor c = getCursor(); 229 c.moveToPosition(position); 230 return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_MAILBOX); 231 } 232 233 private int getAccountUnreadCount(Cursor c) { 234 return getMessageCount(c); 235 } 236 237 /** 238 * Returns the account/mailbox ID extracted from the given cursor. 239 */ 240 private static long getId(Cursor c) { 241 return c.getLong(c.getColumnIndex(EmailContent.RECORD_ID)); 242 } 243 244 /** 245 * @return ID of the account / mailbox for a row 246 */ 247 public long getId(int position) { 248 final Cursor c = getCursor(); 249 return c.moveToPosition(position) ? getId(c) : Account.NO_ACCOUNT; 250 } 251 252 /** 253 * @return ID of the account for a row 254 */ 255 public long getAccountId(int position) { 256 final Cursor c = getCursor(); 257 return c.moveToPosition(position) 258 ? c.getLong(c.getColumnIndex(ACCOUNT_ID)) 259 : Account.NO_ACCOUNT; 260 } 261 262 /** Returns the account name extracted from the given cursor. */ 263 static String getDisplayName(Cursor cursor) { 264 return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME)); 265 } 266 267 /** Returns the email address extracted from the given cursor. */ 268 private static String getAccountEmailAddress(Cursor cursor) { 269 return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS)); 270 } 271 272 /** 273 * Returns the message count (unread or total, depending on row) extracted from the given 274 * cursor. 275 */ 276 private static int getMessageCount(Cursor cursor) { 277 return cursor.getInt(cursor.getColumnIndex(MESSAGE_COUNT)); 278 } 279 280 private static String sCombinedViewDisplayName; 281 private static String getCombinedViewDisplayName(Context c) { 282 if (sCombinedViewDisplayName == null) { 283 sCombinedViewDisplayName = c.getResources().getString( 284 R.string.mailbox_list_account_selector_combined_view); 285 } 286 return sCombinedViewDisplayName; 287 } 288 289 /** 290 * Load the account list. The resulting cursor contains 291 * - Account info 292 * - # of unread messages in inbox 293 * - The "Combined view" row if there's more than one account. 294 */ 295 @VisibleForTesting 296 static class AccountsLoader extends ThrottlingCursorLoader { 297 private final Context mContext; 298 private final long mAccountId; 299 private final long mMailboxId; 300 private final boolean mUseTwoPane; // Injectable for test 301 private final FolderProperties mFolderProperties; 302 303 @VisibleForTesting 304 AccountsLoader(Context context, long accountId, long mailboxId, boolean useTwoPane) { 305 // Super class loads a regular account cursor, but we replace it in loadInBackground(). 306 super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null, 307 ORDER_BY); 308 mContext = context; 309 mAccountId = accountId; 310 mMailboxId = mailboxId; 311 mFolderProperties = FolderProperties.getInstance(mContext); 312 mUseTwoPane = useTwoPane; 313 } 314 315 @Override 316 public Cursor loadInBackground() { 317 final Cursor accountsCursor = super.loadInBackground(); 318 // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed. 319 final CursorWithExtras resultCursor 320 = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor); 321 final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor); 322 addMailboxesToCursor(resultCursor, accountPosition); 323 324 resultCursor.setAccountMailboxInfo(getContext(), mAccountId, mMailboxId); 325 return resultCursor; 326 } 327 328 /** Adds the account list [with extra meta data] to the given matrix cursor */ 329 private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) { 330 int accountPosition = UNKNOWN_POSITION; 331 accountCursor.moveToPosition(-1); 332 333 matrixCursor.mAccountCount = accountCursor.getCount(); 334 int totalUnread = 0; 335 int currentPosition = 1; 336 while (accountCursor.moveToNext()) { 337 // Add account, with its unread count. 338 final long accountId = accountCursor.getLong(0); 339 final int unread = Mailbox.getUnreadCountByAccountAndMailboxType( 340 mContext, accountId, Mailbox.TYPE_INBOX); 341 final String name = getDisplayName(accountCursor); 342 final String emailAddress = getAccountEmailAddress(accountCursor); 343 addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread, 344 UNKNOWN_POSITION, accountId); 345 totalUnread += unread; 346 if (accountId == mAccountId) { 347 accountPosition = currentPosition; 348 } 349 currentPosition++; 350 } 351 // Add "combined view" if more than one account exists 352 final int countAccounts = accountCursor.getCount(); 353 if (countAccounts > 1) { 354 final String accountCount = mContext.getResources().getQuantityString( 355 R.plurals.number_of_accounts, countAccounts, countAccounts); 356 addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW, 357 getCombinedViewDisplayName(mContext), 358 accountCount, totalUnread, UNKNOWN_POSITION, 359 Account.ACCOUNT_ID_COMBINED_VIEW); 360 361 // Increment the account count for the combined account. 362 matrixCursor.mAccountCount++; 363 } 364 return accountPosition; 365 } 366 367 /** 368 * Adds the recent mailbox list / "show all folders" to the given cursor. 369 * 370 * @param matrixCursor the cursor to add the list to 371 * @param accountPosition the cursor position of the currently selected account 372 */ 373 private void addMailboxesToCursor(CursorWithExtras matrixCursor, int accountPosition) { 374 if (mAccountId == Account.NO_ACCOUNT) { 375 return; // Account not selected 376 } 377 if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 378 if (!mUseTwoPane) { 379 // TODO We may want a header for this to separate it from the account list 380 addShowAllFoldersRow(matrixCursor, accountPosition); 381 } 382 return; 383 } 384 String emailAddress = null; 385 if (accountPosition != UNKNOWN_POSITION) { 386 matrixCursor.moveToPosition(accountPosition); 387 emailAddress = 388 matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS)); 389 } 390 RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext); 391 ArrayList<Long> recentMailboxes = null; 392 if (!mUseTwoPane) { 393 // Do not display recent mailboxes in the account spinner for the two pane view 394 recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane); 395 } 396 final int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size(); 397 matrixCursor.mRecentCount = recentCount; 398 399 if (!mUseTwoPane) { 400 // "Recent mailboxes" header 401 addHeaderRow(matrixCursor, mContext.getString( 402 R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress)); 403 } 404 405 if (recentCount > 0) { 406 addMailboxRows(matrixCursor, accountPosition, recentMailboxes); 407 } 408 409 if (!mUseTwoPane) { 410 addShowAllFoldersRow(matrixCursor, accountPosition); 411 } 412 } 413 414 private void addShowAllFoldersRow(CursorWithExtras matrixCursor, int accountPosition) { 415 matrixCursor.mHasShowAllFolders = true; 416 String name = mContext.getString( 417 R.string.mailbox_list_account_selector_show_all_folders); 418 addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0, 419 accountPosition, mAccountId); 420 } 421 422 423 private static final String[] RECENT_MAILBOX_INFO_PROJECTION = new String[] { 424 MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, 425 MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT 426 }; 427 428 private void addMailboxRows(MatrixCursor matrixCursor, int accountPosition, 429 Collection<Long> mailboxIds) { 430 Cursor c = mContext.getContentResolver().query( 431 Mailbox.CONTENT_URI, RECENT_MAILBOX_INFO_PROJECTION, 432 Utility.buildInSelection(MailboxColumns.ID, mailboxIds), null, 433 RecentMailboxManager.RECENT_MAILBOXES_SORT_ORDER); 434 try { 435 c.moveToPosition(-1); 436 while (c.moveToNext()) { 437 addRow(matrixCursor, ROW_TYPE_MAILBOX, 438 c.getLong(c.getColumnIndex(MailboxColumns.ID)), 439 mFolderProperties.getDisplayName(c), null, 440 mFolderProperties.getMessageCount(c), accountPosition, mAccountId); 441 } 442 } finally { 443 c.close(); 444 } 445 } 446 447 private void addHeaderRow(MatrixCursor cursor, String name) { 448 addRow(cursor, ROW_TYPE_HEADER, 0L, name, null, 0, UNKNOWN_POSITION, 449 Account.NO_ACCOUNT); 450 } 451 452 /** Adds a row to the given cursor */ 453 private void addRow(MatrixCursor cursor, int rowType, long id, String name, 454 String emailAddress, int messageCount, int listPosition, long accountId) { 455 cursor.newRow() 456 .add(rowType) 457 .add(id) 458 .add(name) 459 .add(emailAddress) 460 .add(messageCount) 461 .add(listPosition) 462 .add(accountId); 463 } 464 } 465 466 /** Cursor with some extra meta data. */ 467 static class CursorWithExtras extends ClosingMatrixCursor { 468 469 /** Number of account elements, including the combined account row. */ 470 private int mAccountCount; 471 /** Number of recent mailbox elements */ 472 private int mRecentCount; 473 private boolean mHasShowAllFolders; 474 475 private boolean mAccountExists; 476 477 /** 478 * Account ID that's loaded. 479 */ 480 private long mAccountId; 481 private String mAccountDisplayName; 482 483 /** 484 * Mailbox ID that's loaded. 485 */ 486 private long mMailboxId; 487 private String mMailboxDisplayName; 488 private int mMailboxMessageCount; 489 490 @VisibleForTesting 491 CursorWithExtras(String[] columnNames, Cursor innerCursor) { 492 super(columnNames, innerCursor); 493 } 494 495 private static final String[] ACCOUNT_INFO_PROJECTION = new String[] { 496 AccountColumns.DISPLAY_NAME, 497 }; 498 private static final String[] MAILBOX_INFO_PROJECTION = new String[] { 499 MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, 500 MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT 501 }; 502 503 /** 504 * Set the current account/mailbox info. 505 */ 506 @VisibleForTesting 507 void setAccountMailboxInfo(Context context, long accountId, long mailboxId) { 508 mAccountId = accountId; 509 mMailboxId = mailboxId; 510 511 // Get account info 512 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 513 // We need to treat ACCOUNT_ID_COMBINED_VIEW specially... 514 mAccountExists = true; 515 mAccountDisplayName = getCombinedViewDisplayName(context); 516 if (mailboxId != Mailbox.NO_MAILBOX) { 517 setCombinedMailboxInfo(context, mailboxId); 518 } 519 return; 520 } 521 522 mAccountDisplayName = Utility.getFirstRowString(context, 523 ContentUris.withAppendedId(Account.CONTENT_URI, accountId), 524 ACCOUNT_INFO_PROJECTION, null, null, null, 0, null); 525 if (mAccountDisplayName == null) { 526 // Account gone! 527 mAccountExists = false; 528 return; 529 } 530 mAccountExists = true; 531 532 // If mailbox not specified, done. 533 if (mMailboxId == Mailbox.NO_MAILBOX) { 534 return; 535 } 536 // Combined mailbox? 537 // Unfortunately this can happen even when account != ACCOUNT_ID_COMBINED_VIEW, 538 // when you open "starred" on 2-pane on non-combined view. 539 if (mMailboxId < 0) { 540 setCombinedMailboxInfo(context, mailboxId); 541 return; 542 } 543 544 // Get mailbox info 545 final ContentResolver r = context.getContentResolver(); 546 final Cursor mailboxCursor = r.query( 547 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 548 MAILBOX_INFO_PROJECTION, null, null, null); 549 try { 550 if (mailboxCursor.moveToFirst()) { 551 final FolderProperties fp = FolderProperties.getInstance(context); 552 mMailboxDisplayName = fp.getDisplayName(mailboxCursor); 553 mMailboxMessageCount = fp.getMessageCount(mailboxCursor); 554 } 555 } finally { 556 mailboxCursor.close(); 557 } 558 } 559 560 private void setCombinedMailboxInfo(Context context, long mailboxId) { 561 Preconditions.checkState(mailboxId < -1, "Not combined mailbox"); 562 mMailboxDisplayName = FolderProperties.getInstance(context) 563 .getCombinedMailboxName(mMailboxId); 564 565 mMailboxMessageCount = FolderProperties.getMessageCountForCombinedMailbox( 566 context, mailboxId); 567 } 568 569 /** 570 * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION} 571 * if the given ID does not exist. 572 */ 573 int getPosition(long id) { 574 moveToPosition(-1); 575 while(moveToNext()) { 576 if (id == getId(this)) { 577 return getPosition(); 578 } 579 } 580 return UNKNOWN_POSITION; 581 } 582 583 public int getAccountCount() { 584 return mAccountCount; 585 } 586 587 @VisibleForTesting 588 public int getRecentMailboxCount() { 589 return mRecentCount; 590 } 591 592 /** 593 * @return true if the cursor has more than one selectable item so we should enable the 594 * spinner. 595 */ 596 public boolean shouldEnableSpinner() { 597 return mHasShowAllFolders || (mAccountCount + mRecentCount > 1); 598 } 599 600 public long getAccountId() { 601 return mAccountId; 602 } 603 604 public String getAccountDisplayName() { 605 return mAccountDisplayName; 606 } 607 608 @VisibleForTesting 609 public long getMailboxId() { 610 return mMailboxId; 611 } 612 613 public String getMailboxDisplayName() { 614 return mMailboxDisplayName; 615 } 616 617 public int getMailboxMessageCount() { 618 return mMailboxMessageCount; 619 } 620 621 /** 622 * @return {@code true} if the specified accuont exists. 623 */ 624 public boolean accountExists() { 625 return mAccountExists; 626 } 627 } 628} 629