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