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 private ThreePaneLayout mLayout; 102 103 private boolean mIsSearchResult = false; 104 105 /** 106 * The actual return type from the loader. 107 */ 108 public static class MessagesCursor extends CursorWrapper { 109 /** Whether the mailbox is found. */ 110 public final boolean mIsFound; 111 /** {@link Account} that owns the mailbox. Null for combined mailboxes. */ 112 public final Account mAccount; 113 /** {@link Mailbox} for the loaded mailbox. Null for combined mailboxes. */ 114 public final Mailbox mMailbox; 115 /** {@code true} if the account is an EAS account */ 116 public final boolean mIsEasAccount; 117 /** {@code true} if the loaded mailbox can be refreshed. */ 118 public final boolean mIsRefreshable; 119 /** the number of accounts currently configured. */ 120 public final int mCountTotalAccounts; 121 122 private MessagesCursor(Cursor cursor, 123 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 124 boolean isRefreshable, int countTotalAccounts) { 125 super(cursor); 126 mIsFound = found; 127 mAccount = account; 128 mMailbox = mailbox; 129 mIsEasAccount = isEasAccount; 130 mIsRefreshable = isRefreshable; 131 mCountTotalAccounts = countTotalAccounts; 132 } 133 } 134 135 public MessagesAdapter(Context context, Callback callback, boolean isSearchResult) { 136 super(context.getApplicationContext(), null, 0 /* no auto requery */); 137 mResourceHelper = ResourceHelper.getInstance(context); 138 mCallback = callback; 139 mIsSearchResult = isSearchResult; 140 } 141 142 public void setLayout(ThreePaneLayout layout) { 143 mLayout = layout; 144 } 145 146 public void onSaveInstanceState(Bundle outState) { 147 outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet())); 148 } 149 150 public void loadState(Bundle savedInstanceState) { 151 Set<Long> checkedset = getSelectedSet(); 152 checkedset.clear(); 153 for (long l: savedInstanceState.getLongArray(STATE_CHECKED_ITEMS)) { 154 checkedset.add(l); 155 } 156 notifyDataSetChanged(); 157 } 158 159 /** 160 * Set true for combined mailboxes. 161 */ 162 public void setShowColorChips(boolean show) { 163 mShowColorChips = show; 164 } 165 166 public void setQuery(String query) { 167 mQuery = query; 168 } 169 170 public Set<Long> getSelectedSet() { 171 return mSelectedSet; 172 } 173 174 /** 175 * Clear the selection. It's preferable to calling {@link Set#clear()} on 176 * {@link #getSelectedSet()}, because it also notifies observers. 177 */ 178 public void clearSelection() { 179 Set<Long> checkedset = getSelectedSet(); 180 if (checkedset.size() > 0) { 181 checkedset.clear(); 182 notifyDataSetChanged(); 183 } 184 } 185 186 public boolean isSelected(MessageListItem itemView) { 187 return getSelectedSet().contains(itemView.mMessageId); 188 } 189 190 @Override 191 public void bindView(View view, Context context, Cursor cursor) { 192 // Reset the view (in case it was recycled) and prepare for binding 193 MessageListItem itemView = (MessageListItem) view; 194 itemView.bindViewInit(this, mLayout, mIsSearchResult); 195 196 // TODO: just move thise all to a MessageListItem.bindTo(cursor) so that the fields can 197 // be private, and their inter-dependence when they change can be abstracted away. 198 199 // Load the public fields in the view (for later use) 200 itemView.mMessageId = cursor.getLong(COLUMN_ID); 201 itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); 202 final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY); 203 itemView.mAccountId = accountId; 204 205 boolean isRead = cursor.getInt(COLUMN_READ) != 0; 206 boolean readChanged = isRead != itemView.mRead; 207 itemView.mRead = isRead; 208 itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; 209 final int flags = cursor.getInt(COLUMN_FLAGS); 210 itemView.mHasInvite = (flags & Message.FLAG_INCOMING_MEETING_INVITE) != 0; 211 itemView.mHasBeenRepliedTo = (flags & Message.FLAG_REPLIED_TO) != 0; 212 itemView.mHasBeenForwarded = (flags & Message.FLAG_FORWARDED) != 0; 213 itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 214 itemView.setTimestamp(cursor.getLong(COLUMN_DATE)); 215 itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME); 216 itemView.setText( 217 cursor.getString(COLUMN_SUBJECT), cursor.getString(COLUMN_SNIPPET), readChanged); 218 itemView.mColorChipPaint = 219 mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) : null; 220 221 if (mQuery != null && itemView.mSnippet != null) { 222 itemView.mSnippet = 223 TextUtilities.highlightTermsInText(cursor.getString(COLUMN_SNIPPET), mQuery); 224 } 225 } 226 227 @Override 228 public View newView(Context context, Cursor cursor, ViewGroup parent) { 229 MessageListItem item = new MessageListItem(context); 230 item.setVisibility(View.VISIBLE); 231 return item; 232 } 233 234 public void toggleSelected(MessageListItem itemView) { 235 updateSelected(itemView, !isSelected(itemView)); 236 } 237 238 /** 239 * This is used as a callback from the list items, to set the selected state 240 * 241 * <p>Must be called on the UI thread. 242 * 243 * @param itemView the item being changed 244 * @param newSelected the new value of the selected flag (checkbox state) 245 */ 246 private void updateSelected(MessageListItem itemView, boolean newSelected) { 247 if (newSelected) { 248 mSelectedSet.add(itemView.mMessageId); 249 } else { 250 mSelectedSet.remove(itemView.mMessageId); 251 } 252 if (mCallback != null) { 253 mCallback.onAdapterSelectedChanged(itemView, newSelected, mSelectedSet.size()); 254 } 255 } 256 257 /** 258 * This is used as a callback from the list items, to set the favorite state 259 * 260 * <p>Must be called on the UI thread. 261 * 262 * @param itemView the item being changed 263 * @param newFavorite the new value of the favorite flag (star state) 264 */ 265 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 266 changeFavoriteIcon(itemView, newFavorite); 267 if (mCallback != null) { 268 mCallback.onAdapterFavoriteChanged(itemView, newFavorite); 269 } 270 } 271 272 private void changeFavoriteIcon(MessageListItem view, boolean isFavorite) { 273 view.invalidate(); 274 } 275 276 /** 277 * Creates the loader for {@link MessageListFragment}. 278 * 279 * @return always of {@link MessagesCursor}. 280 */ 281 public static Loader<Cursor> createLoader(Context context, MessageListContext listContext) { 282 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 283 Log.d(Logging.LOG_TAG, "MessagesAdapter createLoader listContext=" + listContext); 284 } 285 return listContext.isSearch() 286 ? new SearchCursorLoader(context, listContext) 287 : new MessagesCursorLoader(context, listContext); 288 } 289 290 private static class MessagesCursorLoader extends ThrottlingCursorLoader { 291 protected final Context mContext; 292 private final long mAccountId; 293 private final long mMailboxId; 294 295 public MessagesCursorLoader(Context context, MessageListContext listContext) { 296 // Initialize with no where clause. We'll set it later. 297 super(context, EmailContent.Message.CONTENT_URI, 298 MESSAGE_PROJECTION, null, null, 299 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 300 mContext = context; 301 mAccountId = listContext.mAccountId; 302 mMailboxId = listContext.getMailboxId(); 303 } 304 305 @Override 306 public Cursor loadInBackground() { 307 // Build the where cause (which can't be done on the UI thread.) 308 setSelection(Message.buildMessageListSelection(mContext, mAccountId, mMailboxId)); 309 // Then do a query to get the cursor 310 return loadExtras(super.loadInBackground()); 311 } 312 313 private Cursor loadExtras(Cursor baseCursor) { 314 boolean found = false; 315 Account account = null; 316 Mailbox mailbox = null; 317 boolean isEasAccount = false; 318 boolean isRefreshable = false; 319 320 if (mMailboxId < 0) { 321 // Magic mailbox. 322 found = true; 323 } else { 324 mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 325 if (mailbox != null) { 326 account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); 327 if (account != null) { 328 found = true; 329 isEasAccount = account.isEasAccount(mContext) ; 330 isRefreshable = Mailbox.isRefreshable(mContext, mMailboxId); 331 } else { // Account removed? 332 mailbox = null; 333 } 334 } 335 } 336 final int countAccounts = EmailContent.count(mContext, Account.CONTENT_URI); 337 return wrapCursor(baseCursor, found, account, mailbox, isEasAccount, 338 isRefreshable, countAccounts); 339 } 340 341 /** 342 * Wraps a basic cursor containing raw messages with information about the context of 343 * the list that's being loaded, such as the account and the mailbox the messages 344 * are for. 345 * Subclasses may extend this to wrap with additional data. 346 */ 347 protected Cursor wrapCursor(Cursor cursor, 348 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 349 boolean isRefreshable, int countTotalAccounts) { 350 return new MessagesCursor(cursor, found, account, mailbox, isEasAccount, 351 isRefreshable, countTotalAccounts); 352 } 353 } 354 355 public static class SearchResultsCursor extends MessagesCursor { 356 private final Mailbox mSearchedMailbox; 357 private final int mResultsCount; 358 private SearchResultsCursor(Cursor cursor, 359 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 360 boolean isRefreshable, int countTotalAccounts, 361 Mailbox searchedMailbox, int resultsCount) { 362 super(cursor, found, account, mailbox, isEasAccount, 363 isRefreshable, countTotalAccounts); 364 mSearchedMailbox = searchedMailbox; 365 mResultsCount = resultsCount; 366 } 367 368 /** 369 * @return the total number of results that match the given search query. Note that 370 * there may not be that many items loaded in the cursor yet. 371 */ 372 public int getResultsCount() { 373 return mResultsCount; 374 } 375 376 public Mailbox getSearchedMailbox() { 377 return mSearchedMailbox; 378 } 379 } 380 381 /** 382 * A special loader used to perform a search. 383 */ 384 private static class SearchCursorLoader extends MessagesCursorLoader { 385 private final MessageListContext mListContext; 386 private int mResultsCount = -1; 387 private Mailbox mSearchedMailbox = null; 388 389 public SearchCursorLoader(Context context, MessageListContext listContext) { 390 super(context, listContext); 391 Preconditions.checkArgument(listContext.isSearch()); 392 mListContext = listContext; 393 } 394 395 @Override 396 public Cursor loadInBackground() { 397 if (mResultsCount >= 0) { 398 // Result count known - the initial search meta data must have completed. 399 return super.loadInBackground(); 400 } 401 402 if (mSearchedMailbox == null) { 403 mSearchedMailbox = Mailbox.restoreMailboxWithId( 404 mContext, mListContext.getSearchedMailbox()); 405 } 406 407 // The search results info hasn't even been loaded yet, so the Controller has not yet 408 // initialized the search mailbox properly. Kick off the search first. 409 Controller controller = Controller.getInstance(mContext); 410 try { 411 mResultsCount = controller.searchMessages( 412 mListContext.mAccountId, mListContext.getSearchParams()); 413 } catch (MessagingException e) { 414 } 415 416 // Return whatever the super would do, now that we know the results are ready. 417 // After this point, it should behave as a normal mailbox load for messages. 418 return super.loadInBackground(); 419 } 420 421 @Override 422 protected Cursor wrapCursor(Cursor cursor, 423 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 424 boolean isRefreshable, int countTotalAccounts) { 425 return new SearchResultsCursor(cursor, found, account, mailbox, isEasAccount, 426 isRefreshable, countTotalAccounts, mSearchedMailbox, mResultsCount); 427 } 428 } 429} 430