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