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