MessagesAdapter.java revision 6b256f1fa10b858ae2bea827b65c8608592e4ba3
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.Context; 20import android.content.Loader; 21import android.database.Cursor; 22import android.database.CursorWrapper; 23import android.database.MatrixCursor; 24import android.os.Bundle; 25import android.util.Log; 26import android.view.View; 27import android.view.ViewGroup; 28import android.widget.CursorAdapter; 29 30import com.android.email.Controller; 31import com.android.email.Email; 32import com.android.email.MessageListContext; 33import com.android.email.ResourceHelper; 34import com.android.email.data.ThrottlingCursorLoader; 35import com.android.emailcommon.Logging; 36import com.android.emailcommon.mail.MessagingException; 37import com.android.emailcommon.provider.Account; 38import com.android.emailcommon.provider.EmailContent; 39import com.android.emailcommon.provider.EmailContent.Message; 40import com.android.emailcommon.provider.EmailContent.MessageColumns; 41import com.android.emailcommon.provider.Mailbox; 42import com.android.emailcommon.utility.TextUtilities; 43import com.android.emailcommon.utility.Utility; 44import com.google.common.base.Preconditions; 45 46import java.util.HashSet; 47import java.util.Set; 48 49 50/** 51 * This class implements the adapter for displaying messages based on cursors. 52 */ 53/* package */ class MessagesAdapter extends CursorAdapter { 54 private static final String STATE_CHECKED_ITEMS = 55 "com.android.email.activity.MessagesAdapter.checkedItems"; 56 57 /* package */ static final String[] MESSAGE_PROJECTION = new String[] { 58 EmailContent.RECORD_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, 59 MessageColumns.DISPLAY_NAME, MessageColumns.SUBJECT, MessageColumns.TIMESTAMP, 60 MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_ATTACHMENT, 61 MessageColumns.FLAGS, MessageColumns.SNIPPET 62 }; 63 64 public static final int COLUMN_ID = 0; 65 public static final int COLUMN_MAILBOX_KEY = 1; 66 public static final int COLUMN_ACCOUNT_KEY = 2; 67 public static final int COLUMN_DISPLAY_NAME = 3; 68 public static final int COLUMN_SUBJECT = 4; 69 public static final int COLUMN_DATE = 5; 70 public static final int COLUMN_READ = 6; 71 public static final int COLUMN_FAVORITE = 7; 72 public static final int COLUMN_ATTACHMENTS = 8; 73 public static final int COLUMN_FLAGS = 9; 74 public static final int COLUMN_SNIPPET = 10; 75 76 private final ResourceHelper mResourceHelper; 77 78 /** If true, show color chips. */ 79 private boolean mShowColorChips; 80 81 /** If not null, the query represented by this group of messages */ 82 private String mQuery; 83 84 /** 85 * Set of seleced message IDs. 86 */ 87 private final HashSet<Long> mSelectedSet = new HashSet<Long>(); 88 89 /** 90 * Callback from MessageListAdapter. All methods are called on the UI thread. 91 */ 92 public interface Callback { 93 /** Called when the use starts/unstars a message */ 94 void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite); 95 /** Called when the user selects/unselects a message */ 96 void onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, 97 int mSelectedCount); 98 } 99 100 private final Callback mCallback; 101 102 /** 103 * The actual return type from the loader. 104 */ 105 public static class MessagesCursor extends CursorWrapper { 106 /** Whether the mailbox is found. */ 107 public final boolean mIsFound; 108 /** {@link Account} that owns the mailbox. Null for combined mailboxes. */ 109 public final Account mAccount; 110 /** {@link Mailbox} for the loaded mailbox. Null for combined mailboxes. */ 111 public final Mailbox mMailbox; 112 /** {@code true} if the account is an EAS account */ 113 public final boolean mIsEasAccount; 114 /** {@code true} if the loaded mailbox can be refreshed. */ 115 public final boolean mIsRefreshable; 116 /** the number of accounts currently configured. */ 117 public final int mCountTotalAccounts; 118 119 private MessagesCursor(Cursor cursor, 120 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 121 boolean isRefreshable, int countTotalAccounts) { 122 super(cursor); 123 mIsFound = found; 124 mAccount = account; 125 mMailbox = mailbox; 126 mIsEasAccount = isEasAccount; 127 mIsRefreshable = isRefreshable; 128 mCountTotalAccounts = countTotalAccounts; 129 } 130 } 131 132 public MessagesAdapter(Context context, Callback callback) { 133 super(context.getApplicationContext(), null, 0 /* no auto requery */); 134 mResourceHelper = ResourceHelper.getInstance(context); 135 mCallback = callback; 136 } 137 138 public void onSaveInstanceState(Bundle outState) { 139 outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet())); 140 } 141 142 public void loadState(Bundle savedInstanceState) { 143 Set<Long> checkedset = getSelectedSet(); 144 checkedset.clear(); 145 for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) { 146 checkedset.add(l); 147 } 148 notifyDataSetChanged(); 149 } 150 151 /** 152 * Set true for combined mailboxes. 153 */ 154 public void setShowColorChips(boolean show) { 155 mShowColorChips = show; 156 } 157 158 public void setQuery(String query) { 159 mQuery = query; 160 } 161 162 public Set<Long> getSelectedSet() { 163 return mSelectedSet; 164 } 165 166 /** 167 * Clear the selection. It's preferable to calling {@link Set#clear()} on 168 * {@link #getSelectedSet()}, because it also notifies observers. 169 */ 170 public void clearSelection() { 171 Set<Long> checkedset = getSelectedSet(); 172 if (checkedset.size() > 0) { 173 checkedset.clear(); 174 notifyDataSetChanged(); 175 } 176 } 177 178 public boolean isSelected(MessageListItem itemView) { 179 return getSelectedSet().contains(itemView.mMessageId); 180 } 181 182 @Override 183 public void bindView(View view, Context context, Cursor cursor) { 184 // Reset the view (in case it was recycled) and prepare for binding 185 MessageListItem itemView = (MessageListItem) view; 186 itemView.bindViewInit(this); 187 188 // Load the public fields in the view (for later use) 189 itemView.mMessageId = cursor.getLong(COLUMN_ID); 190 itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); 191 final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY); 192 itemView.mAccountId = accountId; 193 itemView.mRead = cursor.getInt(COLUMN_READ) != 0; 194 itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; 195 itemView.mHasInvite = 196 (cursor.getInt(COLUMN_FLAGS) & Message.FLAG_INCOMING_MEETING_INVITE) != 0; 197 itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 198 itemView.mTimestamp = cursor.getLong(COLUMN_DATE); 199 itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME); 200 itemView.mSnippet = cursor.getString(COLUMN_SNIPPET); 201 itemView.mSubject = cursor.getString(COLUMN_SUBJECT); 202 itemView.mSnippetLineCount = MessageListItem.NEEDS_LAYOUT; 203 itemView.mColorChipPaint = 204 mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) : null; 205 206 if (mQuery != null && itemView.mSnippet != null) { 207 itemView.mSnippet = 208 TextUtilities.highlightTermsInText(cursor.getString(COLUMN_SNIPPET), mQuery); 209 } 210 } 211 212 @Override 213 public View newView(Context context, Cursor cursor, ViewGroup parent) { 214 MessageListItem item = new MessageListItem(context); 215 item.setVisibility(View.VISIBLE); 216 return item; 217 } 218 219 public void toggleSelected(MessageListItem itemView) { 220 updateSelected(itemView, !isSelected(itemView)); 221 } 222 223 /** 224 * This is used as a callback from the list items, to set the selected state 225 * 226 * <p>Must be called on the UI thread. 227 * 228 * @param itemView the item being changed 229 * @param newSelected the new value of the selected flag (checkbox state) 230 */ 231 private void updateSelected(MessageListItem itemView, boolean newSelected) { 232 if (newSelected) { 233 mSelectedSet.add(itemView.mMessageId); 234 } else { 235 mSelectedSet.remove(itemView.mMessageId); 236 } 237 if (mCallback != null) { 238 mCallback.onAdapterSelectedChanged(itemView, newSelected, mSelectedSet.size()); 239 } 240 } 241 242 /** 243 * This is used as a callback from the list items, to set the favorite state 244 * 245 * <p>Must be called on the UI thread. 246 * 247 * @param itemView the item being changed 248 * @param newFavorite the new value of the favorite flag (star state) 249 */ 250 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 251 changeFavoriteIcon(itemView, newFavorite); 252 if (mCallback != null) { 253 mCallback.onAdapterFavoriteChanged(itemView, newFavorite); 254 } 255 } 256 257 private void changeFavoriteIcon(MessageListItem view, boolean isFavorite) { 258 view.invalidate(); 259 } 260 261 /** 262 * Creates the loader for {@link MessageListFragment}. 263 * 264 * @return always of {@link MessagesCursor}. 265 */ 266 public static Loader<Cursor> createLoader(Context context, MessageListContext listContext) { 267 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 268 Log.d(Logging.LOG_TAG, "MessagesAdapter createLoader listContext=" + listContext); 269 } 270 return listContext.isSearch() 271 ? new SearchCursorLoader(context, listContext) 272 : new MessagesCursorLoader(context, listContext.getMailboxId()); 273 } 274 275 private static class MessagesCursorLoader extends ThrottlingCursorLoader { 276 protected final Context mContext; 277 private final long mMailboxId; 278 279 public MessagesCursorLoader(Context context, long mailboxId) { 280 // Initialize with no where clause. We'll set it later. 281 super(context, EmailContent.Message.CONTENT_URI, 282 MESSAGE_PROJECTION, null, null, 283 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 284 mContext = context; 285 mMailboxId = mailboxId; 286 } 287 288 @Override 289 public Cursor loadInBackground() { 290 final Cursor returnCursor; 291 292 // Only perform a load if the selected mailbox can hold messages 293 // box can be null on the combined view where we use negative mailbox ids. 294 final boolean canHaveMessages; 295 if (mMailboxId < 0) { 296 // Combined mailboxes can always have messages. 297 canHaveMessages = true; 298 } else { 299 Mailbox box = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 300 canHaveMessages = (box != null) && (box.mFlags & Mailbox.FLAG_HOLDS_MAIL) != 0; 301 } 302 if (canHaveMessages) { 303 // Build the where cause (which can't be done on the UI thread.) 304 setSelection(Message.buildMessageListSelection(mContext, mMailboxId)); 305 // Then do a query to get the cursor 306 returnCursor = super.loadInBackground(); 307 } else { 308 // return an empty cursor 309 returnCursor = new MatrixCursor(getProjection()); 310 } 311 return loadExtras(returnCursor); 312 } 313 314 private Cursor loadExtras(Cursor baseCursor) { 315 boolean found = false; 316 Account account = null; 317 Mailbox mailbox = null; 318 boolean isEasAccount = false; 319 boolean isRefreshable = false; 320 321 if (mMailboxId < 0) { 322 // Magic mailbox. 323 found = true; 324 } else { 325 mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 326 if (mailbox != null) { 327 account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); 328 if (account != null) { 329 found = true; 330 isEasAccount = account.isEasAccount(mContext) ; 331 isRefreshable = Mailbox.isRefreshable(mContext, mMailboxId); 332 } else { // Account removed? 333 mailbox = null; 334 } 335 } 336 } 337 final int countAccounts = EmailContent.count(mContext, Account.CONTENT_URI); 338 return wrapCursor(baseCursor, found, account, mailbox, isEasAccount, 339 isRefreshable, countAccounts); 340 } 341 342 /** 343 * Wraps a basic cursor containing raw messages with information about the context of 344 * the list that's being loaded, such as the account and the mailbox the messages 345 * are for. 346 * Subclasses may extend this to wrap with additional data. 347 */ 348 protected Cursor wrapCursor(Cursor cursor, 349 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 350 boolean isRefreshable, int countTotalAccounts) { 351 return new MessagesCursor(cursor, found, account, mailbox, isEasAccount, 352 isRefreshable, countTotalAccounts); 353 } 354 } 355 356 public static class SearchResultsCursor extends MessagesCursor { 357 private final int mResultsCount; 358 private SearchResultsCursor(Cursor cursor, 359 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 360 boolean isRefreshable, int countTotalAccounts, int resultsCount) { 361 super(cursor, found, account, mailbox, isEasAccount, 362 isRefreshable, countTotalAccounts); 363 mResultsCount = resultsCount; 364 } 365 366 public int getResultsCount() { 367 return mResultsCount; 368 } 369 370 public boolean hasResults() { 371 return getResultsCount() == 0; 372 } 373 } 374 375 /** 376 * A special loader used to perform a search. 377 */ 378 private static class SearchCursorLoader extends MessagesCursorLoader { 379 private final MessageListContext mListContext; 380 private int mResultsCount = -1; 381 382 public SearchCursorLoader(Context context, MessageListContext listContext) { 383 super(context, listContext.getMailboxId()); 384 Preconditions.checkArgument(listContext.isSearch()); 385 mListContext = listContext; 386 } 387 388 @Override 389 public Cursor loadInBackground() { 390 if (mResultsCount >= 0) { 391 // Result count known - the initial search meta data must have completed. 392 return super.loadInBackground(); 393 } 394 395 // The search results info hasn't even been loaded yet, so the Controller has not yet 396 // initialized the search mailbox properly. Kick off the search first. 397 Controller controller = Controller.getInstance(mContext); 398 try { 399 mResultsCount = controller.searchMessages( 400 mListContext.mAccountId, mListContext.getSearchParams()); 401 } catch (MessagingException e) { 402 } 403 404 // Return whatever the super would do, now that we know the results are ready. 405 // After this point, it should behave as a normal mailbox load for messages. 406 return super.loadInBackground(); 407 } 408 409 @Override 410 protected Cursor wrapCursor(Cursor cursor, 411 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 412 boolean isRefreshable, int countTotalAccounts) { 413 return new SearchResultsCursor(cursor, found, account, mailbox, isEasAccount, 414 isRefreshable, countTotalAccounts, mResultsCount); 415 } 416 } 417} 418