MessagesAdapter.java revision b9b6ce1d36c33e5663c33db191d5dded27df0d5f
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
17package com.android.email.activity;
18
19import android.content.Context;
20import android.content.Loader;
21import android.database.Cursor;
22import android.database.CursorWrapper;
23import android.os.Bundle;
24import android.util.Log;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.CursorAdapter;
28
29import com.android.email.Controller;
30import com.android.email.Email;
31import com.android.email.MessageListContext;
32import com.android.email.ResourceHelper;
33import com.android.email.data.ThrottlingCursorLoader;
34import com.android.emailcommon.Logging;
35import com.android.emailcommon.mail.MessagingException;
36import com.android.emailcommon.provider.Account;
37import com.android.emailcommon.provider.EmailContent;
38import com.android.emailcommon.provider.EmailContent.Message;
39import com.android.emailcommon.provider.EmailContent.MessageColumns;
40import com.android.emailcommon.provider.Mailbox;
41import com.android.emailcommon.utility.TextUtilities;
42import com.android.emailcommon.utility.Utility;
43import com.google.common.base.Preconditions;
44
45import java.util.HashSet;
46import 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