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