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 boolean isAccount = isAccountItem(c); 160 if (displayName.equals(emailAddress) || !isAccount) { 161 emailAddressView.setVisibility(View.GONE); 162 } else { 163 emailAddressView.setVisibility(View.VISIBLE); 164 emailAddressView.setText(emailAddress); 165 } 166 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 we're on a combined account, show the color chip indicators for all real 174 // accounts so it can be used as a legend. 175 boolean isCombinedActive = 176 ((CursorWithExtras) c).getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW; 177 178 if (isCombinedActive && Account.isNormalAccount(id)) { 179 chipView.setBackgroundColor(mResourceHelper.getAccountColor(id)); 180 chipView.setVisibility(View.VISIBLE); 181 } else { 182 chipView.setVisibility(View.GONE); 183 } 184 } else { 185 unreadCountView.setVisibility(View.INVISIBLE); 186 chipView.setVisibility(View.GONE); 187 } 188 189 } 190 return view; 191 } 192 193 @Override 194 public View newView(Context context, Cursor cursor, ViewGroup parent) { 195 return null; // we don't reuse views. This method never gets called. 196 } 197 198 @Override 199 public void bindView(View view, Context context, Cursor cursor) { 200 // we don't reuse views. This method never gets called. 201 } 202 203 @Override 204 public int getViewTypeCount() { 205 return 2; 206 } 207 208 @Override 209 public int getItemViewType(int position) { 210 Cursor c = getCursor(); 211 c.moveToPosition(position); 212 return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER 213 ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER 214 : ITEM_VIEW_TYPE_ACCOUNT; 215 } 216 217 @Override 218 public boolean areAllItemsEnabled() { 219 return false; 220 } 221 222 @Override 223 public boolean isEnabled(int position) { 224 return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER); 225 } 226 227 public boolean isAccountItem(int position) { 228 Cursor c = getCursor(); 229 c.moveToPosition(position); 230 return isAccountItem(c); 231 } 232 233 public boolean isAccountItem(Cursor c) { 234 return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_ACCOUNT); 235 } 236 237 public boolean isMailboxItem(int position) { 238 Cursor c = getCursor(); 239 c.moveToPosition(position); 240 return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_MAILBOX); 241 } 242 243 private int getAccountUnreadCount(Cursor c) { 244 return getMessageCount(c); 245 } 246 247 /** 248 * Returns the account/mailbox ID extracted from the given cursor. 249 */ 250 private static long getId(Cursor c) { 251 return c.getLong(c.getColumnIndex(EmailContent.RECORD_ID)); 252 } 253 254 /** 255 * @return ID of the account / mailbox for a row 256 */ 257 public long getId(int position) { 258 final Cursor c = getCursor(); 259 return c.moveToPosition(position) ? getId(c) : Account.NO_ACCOUNT; 260 } 261 262 /** 263 * @return ID of the account for a row 264 */ 265 public long getAccountId(int position) { 266 final Cursor c = getCursor(); 267 return c.moveToPosition(position) 268 ? c.getLong(c.getColumnIndex(ACCOUNT_ID)) 269 : Account.NO_ACCOUNT; 270 } 271 272 /** Returns the account name extracted from the given cursor. */ 273 static String getDisplayName(Cursor cursor) { 274 return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME)); 275 } 276 277 /** Returns the email address extracted from the given cursor. */ 278 private static String getAccountEmailAddress(Cursor cursor) { 279 return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS)); 280 } 281 282 /** 283 * Returns the message count (unread or total, depending on row) extracted from the given 284 * cursor. 285 */ 286 private static int getMessageCount(Cursor cursor) { 287 return cursor.getInt(cursor.getColumnIndex(MESSAGE_COUNT)); 288 } 289 290 private static String sCombinedViewDisplayName; 291 private static String getCombinedViewDisplayName(Context c) { 292 if (sCombinedViewDisplayName == null) { 293 sCombinedViewDisplayName = c.getResources().getString( 294 R.string.mailbox_list_account_selector_combined_view); 295 } 296 return sCombinedViewDisplayName; 297 } 298 299 /** 300 * Load the account list. The resulting cursor contains 301 * - Account info 302 * - # of unread messages in inbox 303 * - The "Combined view" row if there's more than one account. 304 */ 305 @VisibleForTesting 306 static class AccountsLoader extends ThrottlingCursorLoader { 307 private final Context mContext; 308 private final long mAccountId; 309 private final long mMailboxId; 310 private final boolean mUseTwoPane; // Injectable for test 311 private final FolderProperties mFolderProperties; 312 313 @VisibleForTesting 314 AccountsLoader(Context context, long accountId, long mailboxId, boolean useTwoPane) { 315 // Super class loads a regular account cursor, but we replace it in loadInBackground(). 316 super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null, 317 ORDER_BY); 318 mContext = context; 319 mAccountId = accountId; 320 mMailboxId = mailboxId; 321 mFolderProperties = FolderProperties.getInstance(mContext); 322 mUseTwoPane = useTwoPane; 323 } 324 325 @Override 326 public Cursor loadInBackground() { 327 final Cursor accountsCursor = super.loadInBackground(); 328 // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed. 329 final CursorWithExtras resultCursor 330 = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor); 331 final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor); 332 addMailboxesToCursor(resultCursor, accountPosition); 333 334 resultCursor.setAccountMailboxInfo(getContext(), mAccountId, mMailboxId); 335 return resultCursor; 336 } 337 338 /** Adds the account list [with extra meta data] to the given matrix cursor */ 339 private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) { 340 int accountPosition = UNKNOWN_POSITION; 341 accountCursor.moveToPosition(-1); 342 343 matrixCursor.mAccountCount = accountCursor.getCount(); 344 int totalUnread = 0; 345 while (accountCursor.moveToNext()) { 346 // Add account, with its unread count. 347 final long accountId = accountCursor.getLong(0); 348 final int unread = Mailbox.getUnreadCountByAccountAndMailboxType( 349 mContext, accountId, Mailbox.TYPE_INBOX); 350 final String name = getDisplayName(accountCursor); 351 final String emailAddress = getAccountEmailAddress(accountCursor); 352 addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread, 353 UNKNOWN_POSITION, accountId); 354 totalUnread += unread; 355 if (accountId == mAccountId) { 356 accountPosition = accountCursor.getPosition(); 357 } 358 } 359 // Add "combined view" if more than one account exists 360 final int countAccounts = accountCursor.getCount(); 361 if (countAccounts > 1) { 362 final String accountCount = mContext.getResources().getQuantityString( 363 R.plurals.number_of_accounts, countAccounts, countAccounts); 364 addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW, 365 getCombinedViewDisplayName(mContext), 366 accountCount, totalUnread, UNKNOWN_POSITION, 367 Account.ACCOUNT_ID_COMBINED_VIEW); 368 369 // Increment the account count for the combined account. 370 matrixCursor.mAccountCount++; 371 } 372 return accountPosition; 373 } 374 375 /** 376 * Adds the recent mailbox list / "show all folders" to the given cursor. 377 * 378 * @param matrixCursor the cursor to add the list to 379 * @param accountPosition the cursor position of the currently selected account 380 */ 381 private void addMailboxesToCursor(CursorWithExtras matrixCursor, int accountPosition) { 382 if (mAccountId == Account.NO_ACCOUNT) { 383 return; // Account not selected 384 } 385 if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 386 if (!mUseTwoPane) { 387 // TODO We may want a header for this to separate it from the account list 388 addShowAllFoldersRow(matrixCursor, accountPosition); 389 } 390 return; 391 } 392 String emailAddress = null; 393 if (accountPosition != UNKNOWN_POSITION) { 394 matrixCursor.moveToPosition(accountPosition); 395 emailAddress = 396 matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS)); 397 } 398 RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext); 399 ArrayList<Long> recentMailboxes = null; 400 if (!mUseTwoPane) { 401 // Do not display recent mailboxes in the account spinner for the two pane view 402 recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane); 403 } 404 final int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size(); 405 matrixCursor.mRecentCount = recentCount; 406 407 if (!mUseTwoPane) { 408 // "Recent mailboxes" header 409 addHeaderRow(matrixCursor, mContext.getString( 410 R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress)); 411 } 412 413 if (recentCount > 0) { 414 addMailboxRows(matrixCursor, accountPosition, recentMailboxes); 415 } 416 417 if (!mUseTwoPane) { 418 addShowAllFoldersRow(matrixCursor, accountPosition); 419 } 420 } 421 422 private void addShowAllFoldersRow(CursorWithExtras matrixCursor, int accountPosition) { 423 matrixCursor.mHasShowAllFolders = true; 424 String name = mContext.getString( 425 R.string.mailbox_list_account_selector_show_all_folders); 426 addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0, 427 accountPosition, mAccountId); 428 } 429 430 431 private static final String[] RECENT_MAILBOX_INFO_PROJECTION = new String[] { 432 MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, 433 MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT 434 }; 435 436 private void addMailboxRows(MatrixCursor matrixCursor, int accountPosition, 437 Collection<Long> mailboxIds) { 438 Cursor c = mContext.getContentResolver().query( 439 Mailbox.CONTENT_URI, RECENT_MAILBOX_INFO_PROJECTION, 440 Utility.buildInSelection(MailboxColumns.ID, mailboxIds), null, 441 RecentMailboxManager.RECENT_MAILBOXES_SORT_ORDER); 442 try { 443 c.moveToPosition(-1); 444 while (c.moveToNext()) { 445 addRow(matrixCursor, ROW_TYPE_MAILBOX, 446 c.getLong(c.getColumnIndex(MailboxColumns.ID)), 447 mFolderProperties.getDisplayName(c), null, 448 mFolderProperties.getMessageCount(c), accountPosition, mAccountId); 449 } 450 } finally { 451 c.close(); 452 } 453 } 454 455 private void addHeaderRow(MatrixCursor cursor, String name) { 456 addRow(cursor, ROW_TYPE_HEADER, 0L, name, null, 0, UNKNOWN_POSITION, 457 Account.NO_ACCOUNT); 458 } 459 460 /** Adds a row to the given cursor */ 461 private void addRow(MatrixCursor cursor, int rowType, long id, String name, 462 String emailAddress, int messageCount, int listPosition, long accountId) { 463 cursor.newRow() 464 .add(rowType) 465 .add(id) 466 .add(name) 467 .add(emailAddress) 468 .add(messageCount) 469 .add(listPosition) 470 .add(accountId); 471 } 472 } 473 474 /** Cursor with some extra meta data. */ 475 static class CursorWithExtras extends ClosingMatrixCursor { 476 477 /** Number of account elements, including the combined account row. */ 478 private int mAccountCount; 479 /** Number of recent mailbox elements */ 480 private int mRecentCount; 481 private boolean mHasShowAllFolders; 482 483 private boolean mAccountExists; 484 485 /** 486 * Account ID that's loaded. 487 */ 488 private long mAccountId; 489 private String mAccountDisplayName; 490 491 /** 492 * Mailbox ID that's loaded. 493 */ 494 private long mMailboxId; 495 private String mMailboxDisplayName; 496 private int mMailboxMessageCount; 497 498 @VisibleForTesting 499 CursorWithExtras(String[] columnNames, Cursor innerCursor) { 500 super(columnNames, innerCursor); 501 } 502 503 private static final String[] ACCOUNT_INFO_PROJECTION = new String[] { 504 AccountColumns.DISPLAY_NAME, 505 }; 506 private static final String[] MAILBOX_INFO_PROJECTION = new String[] { 507 MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, 508 MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT 509 }; 510 511 /** 512 * Set the current account/mailbox info. 513 */ 514 @VisibleForTesting 515 void setAccountMailboxInfo(Context context, long accountId, long mailboxId) { 516 mAccountId = accountId; 517 mMailboxId = mailboxId; 518 519 // Get account info 520 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 521 // We need to treat ACCOUNT_ID_COMBINED_VIEW specially... 522 mAccountExists = true; 523 mAccountDisplayName = getCombinedViewDisplayName(context); 524 if (mailboxId != Mailbox.NO_MAILBOX) { 525 setCombinedMailboxInfo(context, mailboxId); 526 } 527 return; 528 } 529 530 mAccountDisplayName = Utility.getFirstRowString(context, 531 ContentUris.withAppendedId(Account.CONTENT_URI, accountId), 532 ACCOUNT_INFO_PROJECTION, null, null, null, 0, null); 533 if (mAccountDisplayName == null) { 534 // Account gone! 535 mAccountExists = false; 536 return; 537 } 538 mAccountExists = true; 539 540 // If mailbox not specified, done. 541 if (mMailboxId == Mailbox.NO_MAILBOX) { 542 return; 543 } 544 // Combined mailbox? 545 // Unfortunately this can happen even when account != ACCOUNT_ID_COMBINED_VIEW, 546 // when you open "starred" on 2-pane on non-combined view. 547 if (mMailboxId < 0) { 548 setCombinedMailboxInfo(context, mailboxId); 549 return; 550 } 551 552 // Get mailbox info 553 final ContentResolver r = context.getContentResolver(); 554 final Cursor mailboxCursor = r.query( 555 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 556 MAILBOX_INFO_PROJECTION, null, null, null); 557 try { 558 if (mailboxCursor.moveToFirst()) { 559 final FolderProperties fp = FolderProperties.getInstance(context); 560 mMailboxDisplayName = fp.getDisplayName(mailboxCursor); 561 mMailboxMessageCount = fp.getMessageCount(mailboxCursor); 562 } 563 } finally { 564 mailboxCursor.close(); 565 } 566 } 567 568 private void setCombinedMailboxInfo(Context context, long mailboxId) { 569 Preconditions.checkState(mailboxId < -1, "Not combined mailbox"); 570 mMailboxDisplayName = FolderProperties.getInstance(context) 571 .getCombinedMailboxName(mMailboxId); 572 573 mMailboxMessageCount = FolderProperties.getMessageCountForCombinedMailbox( 574 context, mailboxId); 575 } 576 577 /** 578 * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION} 579 * if the given ID does not exist. 580 */ 581 int getPosition(long id) { 582 moveToPosition(-1); 583 while(moveToNext()) { 584 if (id == getId(this)) { 585 return getPosition(); 586 } 587 } 588 return UNKNOWN_POSITION; 589 } 590 591 public int getAccountCount() { 592 return mAccountCount; 593 } 594 595 @VisibleForTesting 596 public int getRecentMailboxCount() { 597 return mRecentCount; 598 } 599 600 /** 601 * @return true if the cursor has more than one selectable item so we should enable the 602 * spinner. 603 */ 604 public boolean shouldEnableSpinner() { 605 return mHasShowAllFolders || (mAccountCount + mRecentCount > 1); 606 } 607 608 public long getAccountId() { 609 return mAccountId; 610 } 611 612 public String getAccountDisplayName() { 613 return mAccountDisplayName; 614 } 615 616 @VisibleForTesting 617 public long getMailboxId() { 618 return mMailboxId; 619 } 620 621 public String getMailboxDisplayName() { 622 return mMailboxDisplayName; 623 } 624 625 public int getMailboxMessageCount() { 626 return mMailboxMessageCount; 627 } 628 629 /** 630 * @return {@code true} if the specified accuont exists. 631 */ 632 public boolean accountExists() { 633 return mAccountExists; 634 } 635 } 636} 637