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    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
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
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
142    public void setLayout(ThreePaneLayout layout) {
143        mLayout = layout;
144    }
145
146    public void onSaveInstanceState(Bundle outState) {
147        outState.putLongArray(STATE_CHECKED_ITEMS, Utility.toPrimitiveLongArray(getSelectedSet()));
148    }
149
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     */
162    public void setShowColorChips(boolean show) {
163        mShowColorChips = show;
164    }
165
166    public void setQuery(String query) {
167        mQuery = query;
168    }
169
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     */
178    public void clearSelection() {
179        Set<Long> checkedset = getSelectedSet();
180        if (checkedset.size() > 0) {
181            checkedset.clear();
182            notifyDataSetChanged();
183        }
184    }
185
186    public boolean isSelected(MessageListItem itemView) {
187        return getSelectedSet().contains(itemView.mMessageId);
188    }
189
190    @Override
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
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
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     */
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     */
265    public void updateFavorite(MessageListItem itemView, boolean newFavorite) {
266        changeFavoriteIcon(itemView, newFavorite);
267        if (mCallback != null) {
268            mCallback.onAdapterFavoriteChanged(itemView, newFavorite);
269        }
270    }
271
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     */
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
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
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
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         */
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;
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         */
372        public int getResultsCount() {
373            return mResultsCount;
374        }
375
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
389        public SearchCursorLoader(Context context, MessageListContext listContext) {
390            super(context, listContext);
391            Preconditions.checkArgument(listContext.isSearch());
392            mListContext = listContext;
393        }
394
395        @Override
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
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