• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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