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 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 MessagesCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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 MessagesAdapter(Context context, Callback callback, boolean isSearchResult)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 setLayout(ThreePaneLayout layout)142 public void setLayout(ThreePaneLayout layout) { 143 mLayout = layout; 144 } 145 onSaveInstanceState(Bundle outState)146 public void onSaveInstanceState(Bundle outState) { 147 outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet())); 148 } 149 loadState(Bundle savedInstanceState)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 */ setShowColorChips(boolean show)162 public void setShowColorChips(boolean show) { 163 mShowColorChips = show; 164 } 165 setQuery(String query)166 public void setQuery(String query) { 167 mQuery = query; 168 } 169 getSelectedSet()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 */ clearSelection()178 public void clearSelection() { 179 Set<Long> checkedset = getSelectedSet(); 180 if (checkedset.size() > 0) { 181 checkedset.clear(); 182 notifyDataSetChanged(); 183 } 184 } 185 isSelected(MessageListItem itemView)186 public boolean isSelected(MessageListItem itemView) { 187 return getSelectedSet().contains(itemView.mMessageId); 188 } 189 190 @Override bindView(View view, Context context, Cursor cursor)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 newView(Context context, Cursor cursor, ViewGroup parent)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 toggleSelected(MessageListItem itemView)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 */ updateSelected(MessageListItem itemView, boolean newSelected)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 */ updateFavorite(MessageListItem itemView, boolean newFavorite)265 public void updateFavorite(MessageListItem itemView, boolean newFavorite) { 266 changeFavoriteIcon(itemView, newFavorite); 267 if (mCallback != null) { 268 mCallback.onAdapterFavoriteChanged(itemView, newFavorite); 269 } 270 } 271 changeFavoriteIcon(MessageListItem view, boolean isFavorite)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 */ createLoader(Context context, MessageListContext listContext)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 MessagesCursorLoader(Context context, MessageListContext listContext)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 loadInBackground()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 loadExtras(Cursor baseCursor)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 */ wrapCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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; SearchResultsCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts, Mailbox searchedMailbox, int resultsCount)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 */ getResultsCount()372 public int getResultsCount() { 373 return mResultsCount; 374 } 375 getSearchedMailbox()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 SearchCursorLoader(Context context, MessageListContext listContext)389 public SearchCursorLoader(Context context, MessageListContext listContext) { 390 super(context, listContext); 391 Preconditions.checkArgument(listContext.isSearch()); 392 mListContext = listContext; 393 } 394 395 @Override loadInBackground()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 wrapCursor(Cursor cursor, boolean found, Account account, Mailbox mailbox, boolean isEasAccount, boolean isRefreshable, int countTotalAccounts)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