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 /** 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 // TODO: just move thise all to a MessageListItem.bindTo(cursor) so that the fields can 188 // be private, and their inter-dependence when they change can be abstracted away. 189 190 // Load the public fields in the view (for later use) 191 itemView.mMessageId = cursor.getLong(COLUMN_ID); 192 itemView.mMailboxId = cursor.getLong(COLUMN_MAILBOX_KEY); 193 final long accountId = cursor.getLong(COLUMN_ACCOUNT_KEY); 194 itemView.mAccountId = accountId; 195 196 boolean isRead = cursor.getInt(COLUMN_READ) != 0; 197 boolean readChanged = isRead != itemView.mRead; 198 itemView.mRead = isRead; 199 itemView.mIsFavorite = cursor.getInt(COLUMN_FAVORITE) != 0; 200 final int flags = cursor.getInt(COLUMN_FLAGS); 201 itemView.mHasInvite = (flags & Message.FLAG_INCOMING_MEETING_INVITE) != 0; 202 itemView.mHasBeenRepliedTo = (flags & Message.FLAG_REPLIED_TO) != 0; 203 itemView.mHasBeenForwarded = (flags & Message.FLAG_FORWARDED) != 0; 204 itemView.mHasAttachment = cursor.getInt(COLUMN_ATTACHMENTS) != 0; 205 itemView.setTimestamp(cursor.getLong(COLUMN_DATE)); 206 itemView.mSender = cursor.getString(COLUMN_DISPLAY_NAME); 207 itemView.setText( 208 cursor.getString(COLUMN_SUBJECT), cursor.getString(COLUMN_SNIPPET), readChanged); 209 itemView.mColorChipPaint = 210 mShowColorChips ? mResourceHelper.getAccountColorPaint(accountId) : null; 211 212 if (mQuery != null && itemView.mSnippet != null) { 213 itemView.mSnippet = 214 TextUtilities.highlightTermsInText(cursor.getString(COLUMN_SNIPPET), mQuery); 215 } 216 } 217 218 @Override 219 public View newView(Context context, Cursor cursor, ViewGroup parent) { 220 MessageListItem item = new MessageListItem(context); 221 item.setVisibility(View.VISIBLE); 222 return item; 223 } 224 225 public void toggleSelected(MessageListItem itemView) { 226 updateSelected(itemView, !isSelected(itemView)); 227 } 228 229 /** 230 * This is used as a callback from the list items, to set the selected state 231 * 232 * <p>Must be called on the UI thread. 233 * 234 * @param itemView the item being changed 235 * @param newSelected the new value of the selected flag (checkbox state) 236 */ 237 private void updateSelected(MessageListItem itemView, boolean newSelected) { 238 if (newSelected) { 239 mSelectedSet.add(itemView.mMessageId); 240 } else { 241 mSelectedSet.remove(itemView.mMessageId); 242 } 243 if (mCallback != null) { 244 mCallback.onAdapterSelectedChanged(itemView, newSelected, mSelectedSet.size()); 245 } 246 } 247 248 /** 249 * This is used as a callback from the list items, to set the favorite state 250 * 251 * <p>Must be called on the UI thread. 252 * 253 * @param itemView the item being changed 254 * @param newFavorite the new value of the favorite flag (star state) 255 */ 256 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 257 changeFavoriteIcon(itemView, newFavorite); 258 if (mCallback != null) { 259 mCallback.onAdapterFavoriteChanged(itemView, newFavorite); 260 } 261 } 262 263 private void changeFavoriteIcon(MessageListItem view, boolean isFavorite) { 264 view.invalidate(); 265 } 266 267 /** 268 * Creates the loader for {@link MessageListFragment}. 269 * 270 * @return always of {@link MessagesCursor}. 271 */ 272 public static Loader<Cursor> createLoader(Context context, MessageListContext listContext) { 273 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 274 Log.d(Logging.LOG_TAG, "MessagesAdapter createLoader listContext=" + listContext); 275 } 276 return listContext.isSearch() 277 ? new SearchCursorLoader(context, listContext) 278 : new MessagesCursorLoader(context, listContext.getMailboxId()); 279 } 280 281 private static class MessagesCursorLoader extends ThrottlingCursorLoader { 282 protected final Context mContext; 283 private final long mMailboxId; 284 285 public MessagesCursorLoader(Context context, long mailboxId) { 286 // Initialize with no where clause. We'll set it later. 287 super(context, EmailContent.Message.CONTENT_URI, 288 MESSAGE_PROJECTION, null, null, 289 EmailContent.MessageColumns.TIMESTAMP + " DESC"); 290 mContext = context; 291 mMailboxId = mailboxId; 292 } 293 294 @Override 295 public Cursor loadInBackground() { 296 // Build the where cause (which can't be done on the UI thread.) 297 setSelection(Message.buildMessageListSelection(mContext, mMailboxId)); 298 // Then do a query to get the cursor 299 return loadExtras(super.loadInBackground()); 300 } 301 302 private Cursor loadExtras(Cursor baseCursor) { 303 boolean found = false; 304 Account account = null; 305 Mailbox mailbox = null; 306 boolean isEasAccount = false; 307 boolean isRefreshable = false; 308 309 if (mMailboxId < 0) { 310 // Magic mailbox. 311 found = true; 312 } else { 313 mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId); 314 if (mailbox != null) { 315 account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); 316 if (account != null) { 317 found = true; 318 isEasAccount = account.isEasAccount(mContext) ; 319 isRefreshable = Mailbox.isRefreshable(mContext, mMailboxId); 320 } else { // Account removed? 321 mailbox = null; 322 } 323 } 324 } 325 final int countAccounts = EmailContent.count(mContext, Account.CONTENT_URI); 326 return wrapCursor(baseCursor, found, account, mailbox, isEasAccount, 327 isRefreshable, countAccounts); 328 } 329 330 /** 331 * Wraps a basic cursor containing raw messages with information about the context of 332 * the list that's being loaded, such as the account and the mailbox the messages 333 * are for. 334 * Subclasses may extend this to wrap with additional data. 335 */ 336 protected Cursor wrapCursor(Cursor cursor, 337 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 338 boolean isRefreshable, int countTotalAccounts) { 339 return new MessagesCursor(cursor, found, account, mailbox, isEasAccount, 340 isRefreshable, countTotalAccounts); 341 } 342 } 343 344 public static class SearchResultsCursor extends MessagesCursor { 345 private final Mailbox mSearchedMailbox; 346 private final int mResultsCount; 347 private SearchResultsCursor(Cursor cursor, 348 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 349 boolean isRefreshable, int countTotalAccounts, 350 Mailbox searchedMailbox, int resultsCount) { 351 super(cursor, found, account, mailbox, isEasAccount, 352 isRefreshable, countTotalAccounts); 353 mSearchedMailbox = searchedMailbox; 354 mResultsCount = resultsCount; 355 } 356 357 /** 358 * @return the total number of results that match the given search query. Note that 359 * there may not be that many items loaded in the cursor yet. 360 */ 361 public int getResultsCount() { 362 return mResultsCount; 363 } 364 365 public Mailbox getSearchedMailbox() { 366 return mSearchedMailbox; 367 } 368 } 369 370 /** 371 * A special loader used to perform a search. 372 */ 373 private static class SearchCursorLoader extends MessagesCursorLoader { 374 private final MessageListContext mListContext; 375 private int mResultsCount = -1; 376 private Mailbox mSearchedMailbox = null; 377 378 public SearchCursorLoader(Context context, MessageListContext listContext) { 379 super(context, listContext.getMailboxId()); 380 Preconditions.checkArgument(listContext.isSearch()); 381 mListContext = listContext; 382 } 383 384 @Override 385 public Cursor loadInBackground() { 386 if (mResultsCount >= 0) { 387 // Result count known - the initial search meta data must have completed. 388 return super.loadInBackground(); 389 } 390 391 if (mSearchedMailbox == null) { 392 mSearchedMailbox = Mailbox.restoreMailboxWithId( 393 mContext, mListContext.getSearchedMailbox()); 394 } 395 396 // The search results info hasn't even been loaded yet, so the Controller has not yet 397 // initialized the search mailbox properly. Kick off the search first. 398 Controller controller = Controller.getInstance(mContext); 399 try { 400 mResultsCount = controller.searchMessages( 401 mListContext.mAccountId, mListContext.getSearchParams()); 402 } catch (MessagingException e) { 403 } 404 405 // Return whatever the super would do, now that we know the results are ready. 406 // After this point, it should behave as a normal mailbox load for messages. 407 return super.loadInBackground(); 408 } 409 410 @Override 411 protected Cursor wrapCursor(Cursor cursor, 412 boolean found, Account account, Mailbox mailbox, boolean isEasAccount, 413 boolean isRefreshable, int countTotalAccounts) { 414 return new SearchResultsCursor(cursor, found, account, mailbox, isEasAccount, 415 isRefreshable, countTotalAccounts, mSearchedMailbox, mResultsCount); 416 } 417 } 418 } 419