AccountSelectorAdapter.java revision ce4cce05b2ee5ea2d9629c189a79f7f30778f534
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.R; 22import com.android.email.data.ClosingMatrixCursor; 23import com.android.email.data.ThrottlingCursorLoader; 24import com.android.emailcommon.provider.Account; 25import com.android.emailcommon.provider.EmailContent.MailboxColumns; 26import com.android.emailcommon.provider.Mailbox; 27import com.android.emailcommon.utility.Utility; 28 29import java.util.ArrayList; 30 31import android.content.ContentUris; 32import android.content.Context; 33import android.content.Loader; 34import android.database.Cursor; 35import android.database.MatrixCursor; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.ViewGroup; 39import android.widget.AdapterView; 40import android.widget.CursorAdapter; 41import android.widget.TextView; 42 43/** 44 * Account selector spinner. 45 * 46 * TODO Test it! 47 */ 48public class AccountSelectorAdapter extends CursorAdapter { 49 /** meta data column for an account's unread count */ 50 private static final String UNREAD_COUNT = "unreadCount"; 51 /** meta data column for the row type; used for display purposes */ 52 private static final String ROW_TYPE = "rowType"; 53 /** meta data position of the currently selected account in the drop-down list */ 54 private static final String ACCOUNT_POSITION = "accountPosition"; 55 private static final int ROW_TYPE_HEADER = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER; 56 @SuppressWarnings("unused") 57 private static final int ROW_TYPE_MAILBOX = 0; 58 private static final int ROW_TYPE_ACCOUNT = 1; 59 private static final int ITEM_VIEW_TYPE_ACCOUNT = 0; 60 static final int UNKNOWN_POSITION = -1; 61 /** Projection for account database query */ 62 private static final String[] ACCOUNT_PROJECTION = new String[] { 63 Account.ID, 64 Account.DISPLAY_NAME, 65 Account.EMAIL_ADDRESS, 66 }; 67 /** 68 * Projection used for the selector display; we add meta data that doesn't exist in the 69 * account database, so, this should be a super-set of {@link #ACCOUNT_PROJECTION}. 70 */ 71 private static final String[] ADAPTER_PROJECTION = new String[] { 72 ROW_TYPE, 73 Account.ID, 74 Account.DISPLAY_NAME, 75 Account.EMAIL_ADDRESS, 76 UNREAD_COUNT, 77 ACCOUNT_POSITION, 78 }; 79 80 /** Sort order. Show the default account first. */ 81 private static final String ORDER_BY = 82 Account.IS_DEFAULT + " desc, " + Account.RECORD_ID; 83 84 private final LayoutInflater mInflater; 85 @SuppressWarnings("hiding") 86 private final Context mContext; 87 88 /** 89 * Returns a loader that can populate the account spinner. 90 * @param context a context 91 * @param accountId the ID of the currently viewed account 92 */ 93 public static Loader<Cursor> createLoader(Context context, long accountId) { 94 return new AccountsLoader(context, accountId, UiUtilities.useTwoPane(context)); 95 } 96 97 public AccountSelectorAdapter(Context context) { 98 super(context, null, 0 /* no auto-requery */); 99 mContext = context; 100 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 101 } 102 103 /** 104 * Invoked when the action bar needs the view of the text in the bar itself. The default 105 * is to show just the display name of the cursor at the given position. 106 */ 107 @Override 108 public View getView(int position, View convertView, ViewGroup parent) { 109 if (!isAccountItem(position)) { 110 // asked to show a recent mailbox; instead, show the account associated w/ the mailbox 111 int newPosition = getAccountPosition(position); 112 if (newPosition != UNKNOWN_POSITION) { 113 position = newPosition; 114 } 115 } 116 return super.getView(position, convertView, parent); 117 } 118 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 @Override 135 public View getDropDownView(int position, View convertView, ViewGroup parent) { 136 Cursor c = getCursor(); 137 c.moveToPosition(position); 138 139 View view; 140 if (c.getInt(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER) { 141 view = mInflater.inflate(R.layout.account_selector_dropdown_header, parent, false); 142 final TextView displayNameView = (TextView) view.findViewById(R.id.display_name); 143 final String displayName = getAccountDisplayName(c); 144 displayNameView.setText(displayName); 145 } else { 146 view = mInflater.inflate(R.layout.account_selector_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 = getAccountDisplayName(position); 152 final String emailAddress = getAccountEmailAddress(position); 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(position); 165 long id = getAccountId(c); 166 if (isAccount || id != Mailbox.NO_MAILBOX) { 167 unreadCountView.setVisibility(View.VISIBLE); 168 unreadCountView.setText(UiUtilities.getMessageCountForUi(mContext, 169 getAccountUnreadCount(position), false)); 170 } else { 171 unreadCountView.setVisibility(View.INVISIBLE); 172 } 173 } 174 return view; 175 } 176 177 @Override 178 public void bindView(View view, Context context, Cursor cursor) { 179 TextView textView = (TextView) view.findViewById(R.id.display_name); 180 textView.setText(getAccountDisplayName(cursor)); 181 } 182 183 @Override 184 public View newView(Context context, Cursor cursor, ViewGroup parent) { 185 return mInflater.inflate(R.layout.account_selector, parent, false); 186 } 187 188 @Override 189 public int getViewTypeCount() { 190 return 2; 191 } 192 193 @Override 194 public int getItemViewType(int position) { 195 Cursor c = getCursor(); 196 c.moveToPosition(position); 197 return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER 198 ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER 199 : ITEM_VIEW_TYPE_ACCOUNT; 200 } 201 202 @Override 203 public boolean isEnabled(int position) { 204 return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER); 205 } 206 207 public boolean isAccountItem(int position) { 208 Cursor c = getCursor(); 209 c.moveToPosition(position); 210 return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_ACCOUNT); 211 } 212 213 public boolean isMailboxItem(int position) { 214 Cursor c = getCursor(); 215 c.moveToPosition(position); 216 return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_MAILBOX); 217 } 218 219 private String getAccountDisplayName(int position) { 220 final Cursor c = getCursor(); 221 return c.moveToPosition(position) ? getAccountDisplayName(c) : null; 222 } 223 224 private String getAccountEmailAddress(int position) { 225 final Cursor c = getCursor(); 226 return c.moveToPosition(position) ? getAccountEmailAddress(c) : null; 227 } 228 229 private int getAccountUnreadCount(int position) { 230 final Cursor c = getCursor(); 231 return c.moveToPosition(position) ? getAccountUnreadCount(c) : 0; 232 } 233 234 int getAccountPosition(int position) { 235 final Cursor c = getCursor(); 236 return c.moveToPosition(position) ? getAccountPosition(c) : UNKNOWN_POSITION; 237 } 238 239 /** Returns the account ID extracted from the given cursor. */ 240 private static long getAccountId(Cursor c) { 241 return c.getLong(c.getColumnIndex(Account.ID)); 242 } 243 244 /** Returns the account name extracted from the given cursor. */ 245 static String getAccountDisplayName(Cursor cursor) { 246 return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME)); 247 } 248 249 /** Returns the email address extracted from the given cursor. */ 250 private static String getAccountEmailAddress(Cursor cursor) { 251 return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS)); 252 } 253 254 /** Returns the unread count extracted from the given cursor. */ 255 private static int getAccountUnreadCount(Cursor cursor) { 256 return cursor.getInt(cursor.getColumnIndex(UNREAD_COUNT)); 257 } 258 259 /** Returns the account position extracted from the given cursor. */ 260 private static int getAccountPosition(Cursor cursor) { 261 return cursor.getInt(cursor.getColumnIndex(ACCOUNT_POSITION)); 262 } 263 264 /** 265 * Load the account list. The resulting cursor contains 266 * - Account info 267 * - # of unread messages in inbox 268 * - The "Combined view" row if there's more than one account. 269 */ 270 @VisibleForTesting 271 static class AccountsLoader extends ThrottlingCursorLoader { 272 private final Context mContext; 273 private final long mAccountId; 274 private final boolean mUseTwoPane; // Injectable for test 275 276 @VisibleForTesting 277 AccountsLoader(Context context, long accountId, boolean useTwoPane) { 278 // Super class loads a regular account cursor, but we replace it in loadInBackground(). 279 super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null, 280 ORDER_BY); 281 mContext = context; 282 mAccountId = accountId; 283 mUseTwoPane = useTwoPane; 284 } 285 286 @Override 287 public Cursor loadInBackground() { 288 final Cursor accountsCursor = super.loadInBackground(); 289 // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed. 290 final CursorWithExtras resultCursor 291 = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor); 292 final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor); 293 addRecentsToCursor(resultCursor, accountPosition); 294 return Utility.CloseTraceCursorWrapper.get(resultCursor); 295 } 296 297 /** Adds the account list [with extra meta data] to the given matrix cursor */ 298 private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) { 299 int accountPosition = UNKNOWN_POSITION; 300 accountCursor.moveToPosition(-1); 301 // Add a header for the accounts 302 String header = 303 mContext.getString(R.string.mailbox_list_account_selector_account_header); 304 addRow(matrixCursor, ROW_TYPE_HEADER, 0L, header, null, 0, UNKNOWN_POSITION); 305 306 matrixCursor.mAccountCount = accountCursor.getCount(); 307 int totalUnread = 0; 308 int currentPosition = 1; 309 while (accountCursor.moveToNext()) { 310 // Add account, with its unread count. 311 final long accountId = accountCursor.getLong(0); 312 final int unread = Mailbox.getUnreadCountByAccountAndMailboxType( 313 mContext, accountId, Mailbox.TYPE_INBOX); 314 final String name = getAccountDisplayName(accountCursor); 315 final String emailAddress = getAccountEmailAddress(accountCursor); 316 addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread, 317 UNKNOWN_POSITION); 318 totalUnread += unread; 319 if (accountId == mAccountId) { 320 accountPosition = currentPosition; 321 } 322 currentPosition++; 323 } 324 // Add "combined view" if more than one account exists 325 final int countAccounts = accountCursor.getCount(); 326 if (countAccounts > 1) { 327 final String name = mContext.getResources().getString( 328 R.string.mailbox_list_account_selector_combined_view); 329 final String accountCount = mContext.getResources().getQuantityString( 330 R.plurals.number_of_accounts, countAccounts, countAccounts); 331 addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW, 332 name, accountCount, totalUnread, UNKNOWN_POSITION); 333 334 // Increment the account count for the combined account. 335 matrixCursor.mAccountCount++; 336 } 337 return accountPosition; 338 } 339 340 /** 341 * Adds the recent mailbox list to the given cursor. 342 * @param matrixCursor the cursor to add the list to 343 * @param accountPosition the cursor position of the currently selected account 344 */ 345 private void addRecentsToCursor(CursorWithExtras matrixCursor, int accountPosition) { 346 if (mAccountId <= 0L || mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) { 347 // Currently selected account isn't usable for our purposes 348 return; 349 } 350 String emailAddress = null; 351 if (accountPosition != UNKNOWN_POSITION) { 352 matrixCursor.moveToPosition(accountPosition); 353 emailAddress = 354 matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS)); 355 } 356 RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext); 357 ArrayList<Long> recentMailboxes = null; 358 if (!mUseTwoPane) { 359 // Do not display recent mailboxes in the account spinner for the two pane view 360 recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane); 361 } 362 int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size(); 363 matrixCursor.mRecentCount = recentCount; 364 if (recentCount > 0) { 365 String mailboxHeader = mContext.getString( 366 R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress); 367 addRow(matrixCursor, ROW_TYPE_HEADER, 0L, mailboxHeader, null, 0, UNKNOWN_POSITION); 368 for (long mailboxId : recentMailboxes) { 369 final int unread = Utility.getFirstRowInt(mContext, 370 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 371 new String[] { MailboxColumns.UNREAD_COUNT }, null, null, null, 0); 372 final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); 373 addRow(matrixCursor, ROW_TYPE_MAILBOX, mailboxId, mailbox.mDisplayName, null, 374 unread, accountPosition); 375 } 376 } else if (!mUseTwoPane) { 377 // Add the header for 'show all folders' 378 String mailboxHeader = mContext.getString( 379 R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress); 380 addRow(matrixCursor, ROW_TYPE_HEADER, 0L, mailboxHeader, null, 0, UNKNOWN_POSITION); 381 } 382 if (!mUseTwoPane) { 383 String name = mContext.getString( 384 R.string.mailbox_list_account_selector_show_all_folders); 385 addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0, 386 accountPosition); 387 } 388 } 389 390 /** Adds a row to the given cursor */ 391 private void addRow(MatrixCursor cursor, int rowType, long id, String name, 392 String emailAddress, int unreadCount, int listPosition) { 393 cursor.newRow() 394 .add(rowType) 395 .add(id) 396 .add(name) 397 .add(emailAddress) 398 .add(unreadCount) 399 .add(listPosition); 400 } 401 } 402 403 /** Cursor with some extra meta data. */ 404 static class CursorWithExtras extends ClosingMatrixCursor { 405 /** Number of account elements, including the combined account row. */ 406 int mAccountCount; 407 /** Number of recent mailbox elements */ 408 int mRecentCount; 409 410 private CursorWithExtras(String[] columnNames, Cursor innerCursor) { 411 super(columnNames, innerCursor); 412 } 413 414 /** 415 * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION} 416 * if the given ID does not exist. 417 */ 418 int getPosition(long id) { 419 moveToPosition(-1); 420 while(moveToNext()) { 421 if (id == getAccountId(this)) { 422 return getPosition(); 423 } 424 } 425 return UNKNOWN_POSITION; 426 } 427 } 428} 429