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 */ onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite)93 void onAdapterFavoriteChanged(MessageListItem itemView, boolean newFavorite); 94 /** Called when the user selects/unselects a message */ onAdapterSelectedChanged(MessageListItem itemView, boolean newSelected, int mSelectedCount)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 MessagesCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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 MessagesAdapter(Context context, Callback callback)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 onSaveInstanceState(Bundle outState)137 public void onSaveInstanceState(Bundle outState) { 138 outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet())); 139 } 140 loadState(Bundle savedInstanceState)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 */ setShowColorChips(boolean show)153 public void setShowColorChips(boolean show) { 154 mShowColorChips = show; 155 } 156 setQuery(String query)157 public void setQuery(String query) { 158 mQuery = query; 159 } 160 getSelectedSet()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 */ clearSelection()169 public void clearSelection() { 170 Set<Long> checkedset = getSelectedSet(); 171 if (checkedset.size() > 0) { 172 checkedset.clear(); 173 notifyDataSetChanged(); 174 } 175 } 176 isSelected(MessageListItem itemView)177 public boolean isSelected(MessageListItem itemView) { 178 return getSelectedSet().contains(itemView.mMessageId); 179 } 180 181 @Override bindView(View view, Context context, Cursor cursor)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 newView(Context context, Cursor cursor, ViewGroup parent)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 toggleSelected(MessageListItem itemView)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 */ updateSelected(MessageListItem itemView, boolean newSelected)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 */ updateFavorite(MessageListItem itemView, boolean newFavorite)256 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 257 changeFavoriteIcon(itemView, newFavorite); 258 if (mCallback != null) { 259 mCallback.onAdapterFavoriteChanged(itemView, newFavorite); 260 } 261 } 262 changeFavoriteIcon(MessageListItem view, boolean isFavorite)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 */ createLoader(Context context, MessageListContext listContext)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 MessagesCursorLoader(Context context, long mailboxId)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 loadInBackground()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 loadExtras(Cursor baseCursor)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 */ wrapCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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; SearchResultsCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts, Mailbox searchedMailbox, int resultsCount)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 */ getResultsCount()361 public int getResultsCount() { 362 return mResultsCount; 363 } 364 getSearchedMailbox()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 SearchCursorLoader(Context context, MessageListContext listContext)378 public SearchCursorLoader(Context context, MessageListContext listContext) { 379 super(context, listContext.getMailboxId()); 380 Preconditions.checkArgument(listContext.isSearch()); 381 mListContext = listContext; 382 } 383 384 @Override loadInBackground()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 wrapCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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