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