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