MailboxFragmentAdapter.java revision 85bd26e21069b6ee3188be034025c207f4686684
1/*
2 * Copyright (C) 2011 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 com.google.common.annotations.VisibleForTesting;
20
21import com.android.email.Email;
22import com.android.email.FolderProperties;
23import com.android.email.R;
24import com.android.email.ResourceHelper;
25import com.android.email.data.ClosingMatrixCursor;
26import com.android.email.data.ThrottlingCursorLoader;
27import com.android.emailcommon.Logging;
28import com.android.emailcommon.provider.EmailContent;
29import com.android.emailcommon.provider.EmailContent.Account;
30import com.android.emailcommon.provider.EmailContent.AccountColumns;
31import com.android.emailcommon.provider.EmailContent.MailboxColumns;
32import com.android.emailcommon.provider.EmailContent.Message;
33import com.android.emailcommon.provider.Mailbox;
34import com.android.emailcommon.utility.Utility;
35
36import java.util.ArrayList;
37
38import android.content.ContentUris;
39import android.content.Context;
40import android.content.Loader;
41import android.database.Cursor;
42import android.database.CursorWrapper;
43import android.database.MatrixCursor;
44import android.database.MatrixCursor.RowBuilder;
45import android.database.MergeCursor;
46import android.util.Log;
47import android.view.LayoutInflater;
48import android.view.View;
49import android.view.ViewGroup;
50import android.widget.AdapterView;
51import android.widget.CursorAdapter;
52import android.widget.ImageView;
53import android.widget.TextView;
54
55/**
56 * Mailbox cursor adapter for the mailbox list fragment.
57 *
58 * A mailbox cursor may contain one of several different types of data. Currently, this
59 * adapter supports the following views:
60 * 1. The standard inbox, mailbox view
61 * 2. The combined mailbox view
62 * 3. Nested folder navigation
63 *
64 * TODO At a minimum, we should break out the loaders. They have no relation to the view code
65 * and only serve to confuse the user.
66 * TODO Determine if we actually need a separate adapter / view / loader for nested folder
67 * navigation. It's a little convoluted at the moment, but, still manageable.
68 */
69class MailboxFragmentAdapter extends CursorAdapter {
70    /**
71     * Callback interface used to report clicks other than the basic list item click or long press.
72     */
73    interface Callback {
74        /** Callback for setting background of mailbox list items during a drag */
75        public void onBind(MailboxListItem listItem);
76    }
77
78    /** Do-nothing callback to avoid null tests for <code>mCallback</code>. */
79    private static final class EmptyCallback implements Callback {
80        public static final Callback INSTANCE = new EmptyCallback();
81        @Override public void onBind(MailboxListItem listItem) { }
82    }
83
84    /*
85     * The type of the row to present to the user. There are 4 defined rows that each
86     * have a slightly different look. These are typically used in the constant column
87     * {@link #ROW_TYPE} specified in {@link #PROJECTION} and {@link #SUBMAILBOX_PROJECTION}.
88     */
89    /** Both regular and combined mailboxes */
90    private static final int ROW_TYPE_MAILBOX = 0;
91    /** Account "mailboxes" in the combined view */
92    private static final int ROW_TYPE_ACCOUNT = 1;
93    // STOPSHIP Need to determine if these types are sufficient for nested folders
94    // The following types are used when drilling into a mailbox
95    /** The current mailbox */
96    private static final int ROW_TYPE_CURMAILBOX = 2;
97    /** Sub mailboxes */
98    private static final int ROW_TYPE_SUBMAILBOX = 3;
99    /** Header */
100    private static final int ROW_TYPE_HEADER = 4;
101
102    /** The type of count to use. Different mailboxes have different counts. */
103    private static final int COUNT_TYPE_UNREAD = 0;
104    private static final int COUNT_TYPE_TOTAL = 1;
105    private static final int COUNT_TYPE_NO_COUNT = 2;
106
107    /** The type of data contained in the cursor row. */
108    private static final String ROW_TYPE = "rowType";
109    /** The original ID of the cursor row. May be negative. */
110    private static final String ORIGINAL_ID = "orgMailboxId";
111    /**
112     * Projection for a typical mailbox or account row.
113     * <p><em>NOTE</em> This projection contains two ID columns. The first, named "_id", is used
114     * by the framework ListView implementation. Since ListView does not handle negative IDs in
115     * this column, we define a "mailbox_id" column that contains the real mailbox ID; which
116     * may be negative for special mailboxes.
117     */
118    private static final String[] PROJECTION = new String[] { MailboxColumns.ID,
119            MailboxColumns.ID + " AS " + ORIGINAL_ID,
120            MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, MailboxColumns.UNREAD_COUNT,
121            MailboxColumns.MESSAGE_COUNT, ROW_TYPE_MAILBOX + " AS " + ROW_TYPE,
122            MailboxColumns.FLAGS, MailboxColumns.ACCOUNT_KEY };
123    // STOPSHIP May need to adjust sub-folder projection depending upon final UX
124    /**
125     * Projection used to retrieve immediate children for a mailbox. The columns need to
126     * be identical to those in {@link #PROJECTION}. We are only changing the constant
127     * column {@link #ROW_TYPE}.
128     */
129    private static final String[] SUBMAILBOX_PROJECTION = new String[] { MailboxColumns.ID,
130        MailboxColumns.ID + " AS " + ORIGINAL_ID,
131        MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, MailboxColumns.UNREAD_COUNT,
132        MailboxColumns.MESSAGE_COUNT, ROW_TYPE_SUBMAILBOX + " AS " + ROW_TYPE,
133        MailboxColumns.FLAGS, MailboxColumns.ACCOUNT_KEY };
134    private static final String[] CURMAILBOX_PROJECTION = new String[] { MailboxColumns.ID,
135        MailboxColumns.ID + " AS " + ORIGINAL_ID,
136        MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE, MailboxColumns.UNREAD_COUNT,
137        MailboxColumns.MESSAGE_COUNT, ROW_TYPE_CURMAILBOX + " AS " + ROW_TYPE,
138        MailboxColumns.FLAGS, MailboxColumns.ACCOUNT_KEY };
139    /** Project to use for matrix cursors; rows MUST be identical to {@link #PROJECTION} */
140    private static final String[] MATRIX_PROJECTION = new String[] {
141        MailboxColumns.ID, ORIGINAL_ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
142        MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT, ROW_TYPE, MailboxColumns.FLAGS,
143        MailboxColumns.ACCOUNT_KEY };
144
145    /** All mailboxes for the account */
146    private static final String ALL_MAILBOX_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?" +
147            " AND " + Mailbox.USER_VISIBLE_MAILBOX_SELECTION;
148    /** All system mailboxes for an account */
149    private static final String SYSTEM_MAILBOX_SELECTION = ALL_MAILBOX_SELECTION +
150            " AND " + MailboxColumns.TYPE + "!=" + Mailbox.TYPE_MAIL;
151    /** All mailboxes with the given parent */
152    private static final String USER_MAILBOX_SELECTION_WITH_PARENT = ALL_MAILBOX_SELECTION +
153            " AND " + MailboxColumns.PARENT_KEY + "=?" +
154            " AND " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_MAIL;
155    /** Selection for a specific mailbox */
156    private static final String MAILBOX_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?" +
157            " AND " + MailboxColumns.ID + "=?";
158
159    private static final String MAILBOX_ORDER_BY = "CASE " + MailboxColumns.TYPE +
160            " WHEN " + Mailbox.TYPE_INBOX   + " THEN 0" +
161            " WHEN " + Mailbox.TYPE_DRAFTS  + " THEN 1" +
162            " WHEN " + Mailbox.TYPE_OUTBOX  + " THEN 2" +
163            " WHEN " + Mailbox.TYPE_SENT    + " THEN 3" +
164            " WHEN " + Mailbox.TYPE_TRASH   + " THEN 4" +
165            " WHEN " + Mailbox.TYPE_JUNK    + " THEN 5" +
166            // Other mailboxes (i.e. of Mailbox.TYPE_MAIL) are shown in alphabetical order.
167            " ELSE 10 END" +
168            " ," + MailboxColumns.DISPLAY_NAME;
169
170    /** View is of a "normal" row */
171    private static final int ITEM_VIEW_TYPE_NORMAL = 0;
172    /** View is of a separator row */
173    private static final int ITEM_VIEW_TYPE_HEADER = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
174
175    private static boolean sEnableUpdate = true;
176    private final LayoutInflater mInflater;
177    private final ResourceHelper mResourceHelper;
178    private final Callback mCallback;
179
180    public MailboxFragmentAdapter(Context context, Callback callback) {
181        super(context, null, 0 /* flags; no content observer */);
182        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
183        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
184        mResourceHelper = ResourceHelper.getInstance(context);
185    }
186
187    @Override
188    public int getViewTypeCount() {
189        return 2;
190    }
191
192    @Override
193    public int getItemViewType(int position) {
194        return isHeader(position) ? ITEM_VIEW_TYPE_HEADER : ITEM_VIEW_TYPE_NORMAL;
195    }
196
197    @Override
198    public boolean isEnabled(int position) {
199        return !isHeader(position);
200    }
201
202    @Override
203    public void bindView(View view, Context context, Cursor cursor) {
204        if (view instanceof MailboxListItem) {
205            bindListItem(view, context, cursor);
206        } else {
207            bindListHeader(view, context, cursor);
208        }
209    }
210
211    @Override
212    public View newView(Context context, Cursor cursor, ViewGroup parent) {
213        if (cursor.getInt(cursor.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER) {
214            return mInflater.inflate(R.layout.mailbox_list_header, parent, false);
215        }
216        return mInflater.inflate(R.layout.mailbox_list_item, parent, false);
217    }
218
219    private boolean isHeader(int position) {
220        Cursor c = getCursor();
221        c.moveToPosition(position);
222        int rowType = c.getInt(c.getColumnIndex(ROW_TYPE));
223        return rowType == ROW_TYPE_HEADER;
224    }
225
226    /** Returns {@code true} if the specified row is of an account in the combined view. */
227    boolean isAccountRow(int position) {
228        return isAccountRow((Cursor) getItem(position));
229    }
230
231    /** Returns {@code true} if the current row is of an account in the combined view. */
232    private static boolean isAccountRow(Cursor cursor) {
233        return getRowType(cursor) == ROW_TYPE_ACCOUNT;
234    }
235
236    /**
237     * Returns the type of count to use for the given row.
238     */
239    private int getCountType(Cursor cursor) {
240        if (isAccountRow(cursor)) {
241            return COUNT_TYPE_UNREAD;
242        }
243        // otherwise, look at mailbox type to determine the count
244        switch (cursor.getInt(cursor.getColumnIndex(MailboxColumns.TYPE))) {
245            case Mailbox.TYPE_DRAFTS:
246            case Mailbox.TYPE_OUTBOX:
247                return COUNT_TYPE_TOTAL;
248            case Mailbox.TYPE_SENT:
249            case Mailbox.TYPE_TRASH:
250                return COUNT_TYPE_NO_COUNT;
251        }
252        return COUNT_TYPE_UNREAD;
253    }
254
255    /**
256     * Returns the unread count for the given row if the count type is {@link #COUNT_TYPE_UNREAD}.
257     * Otherwise, {@code 0}.
258     */
259    int getUnreadCount(int position) {
260        Cursor c = (Cursor) getItem(position);
261        if (getCountType(c) != COUNT_TYPE_UNREAD) {
262            return 0; // Don't have a unread count.
263        }
264        return c.getInt(c.getColumnIndex(MailboxColumns.UNREAD_COUNT));
265    }
266
267    /**
268     * Returns the display name for the given row.
269     */
270    String getDisplayName(Context context, int position) {
271        Cursor c = (Cursor) getItem(position);
272        return getDisplayName(context, c);
273    }
274
275    /**
276     * Returns the ID of the given row. It may be a mailbox or account ID depending upon the
277     * result of {@link #isAccountRow}.
278     */
279    long getId(int position) {
280        Cursor c = (Cursor) getItem(position);
281        return getId(c);
282    }
283
284    /**
285     * Returns the account ID of the mailbox owner for the given row. If the given row is a
286     * combined mailbox, {@link Account#ACCOUNT_ID_COMBINED_VIEW} is returned. If the given
287     * row is an account, returns the account's ID [the same as {@link #ORIGINAL_ID}].
288     */
289    long getAccountId(int position) {
290        Cursor c = (Cursor) getItem(position);
291        return getAccountId(c);
292    }
293
294    /**
295     * Turn on and off list updates; during a drag operation, we do NOT want to the list of
296     * mailboxes to update, as this would be visually jarring
297     * @param state whether or not the MailboxList can be updated
298     */
299    static void enableUpdates(boolean state) {
300        sEnableUpdate = state;
301    }
302
303    private static String getDisplayName(Context context, Cursor cursor) {
304        String name = null;
305        if (getRowType(cursor) == ROW_TYPE_MAILBOX) {
306            // If it's a mailbox (as opposed to account row in combined view), and of certain types,
307            // we use the predefined names.
308            final int type = getType(cursor);
309            final long mailboxId = getId(cursor);
310            name = FolderProperties.getInstance(context).getDisplayName(type, mailboxId);
311        }
312        if (name == null) {
313            name = cursor.getString(cursor.getColumnIndex(MailboxColumns.DISPLAY_NAME));
314        }
315        return name;
316    }
317
318    static long getId(Cursor cursor) {
319        return cursor.getLong(cursor.getColumnIndex(ORIGINAL_ID));
320    }
321
322    static int getType(Cursor cursor) {
323        return cursor.getInt(cursor.getColumnIndex(MailboxColumns.TYPE));
324    }
325
326    static int getMessageCount(Cursor cursor) {
327        return cursor.getInt(cursor.getColumnIndex(MailboxColumns.MESSAGE_COUNT));
328    }
329
330    static int getUnreadCount(Cursor cursor) {
331        return cursor.getInt(cursor.getColumnIndex(MailboxColumns.UNREAD_COUNT));
332    }
333
334    static long getAccountId(Cursor cursor) {
335        return cursor.getLong(cursor.getColumnIndex(MailboxColumns.ACCOUNT_KEY));
336    }
337
338    private static int getRowType(Cursor cursor) {
339        return cursor.getInt(cursor.getColumnIndex(ROW_TYPE));
340    }
341
342    private static int getFlags(Cursor cursor) {
343        return cursor.getInt(cursor.getColumnIndex(MailboxColumns.FLAGS));
344    }
345
346    /**
347     * {@link Cursor} with extra information which is returned by the loader created by
348     * {@link MailboxFragmentAdapter#createMailboxesLoader}.
349     */
350    static class CursorWithExtras extends CursorWrapper {
351        /**
352         * The number of mailboxes in the cursor if the cursor contains top-level mailboxes.
353         * Otherwise, the number of *child* mailboxes.
354         */
355        public final int mChildCount;
356
357        CursorWithExtras(Cursor cursor, int childCount) {
358            super(cursor);
359            mChildCount = childCount;
360        }
361    }
362
363    private void bindListHeader(View view, Context context, Cursor cursor) {
364        final TextView nameView = (TextView) view.findViewById(R.id.display_name);
365        nameView.setText(getDisplayName(context, cursor));
366    }
367
368    private void bindListItem(View view, Context context, Cursor cursor) {
369        final boolean isAccount = isAccountRow(cursor);
370        final int type = getType(cursor);
371        final long id = getId(cursor);
372        final long accountId = getAccountId(cursor);
373        final int flags = getFlags(cursor);
374        final int rowType = getRowType(cursor);
375        final boolean hasVisibleChildren = (flags & Mailbox.FLAG_HAS_CHILDREN) != 0
376                && (flags & Mailbox.FLAG_CHILDREN_VISIBLE) != 0;
377
378        MailboxListItem listItem = (MailboxListItem)view;
379        listItem.mMailboxId = isAccountRow(cursor) ? Mailbox.NO_MAILBOX : id;
380        listItem.mMailboxType = type;
381        listItem.mAccountId = accountId;
382        listItem.mIsValidDropTarget = (id >= 0)
383                && !Utility.arrayContains(Mailbox.INVALID_DROP_TARGETS, type)
384                && (flags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) != 0;
385        listItem.mIsNavigable = hasVisibleChildren;
386
387        listItem.mAdapter = this;
388        // Set the background depending on whether we're in drag mode, the mailbox is a valid
389        // target, etc.
390        mCallback.onBind(listItem);
391
392        // Set mailbox name
393        final TextView nameView = (TextView) view.findViewById(R.id.mailbox_name);
394        nameView.setText(getDisplayName(context, cursor));
395        // Set count
396        final int count;
397        switch (getCountType(cursor)) {
398            case COUNT_TYPE_UNREAD:
399                count = getUnreadCount(cursor);
400                break;
401            case COUNT_TYPE_TOTAL:
402                count = getMessageCount(cursor);
403                break;
404            default: // no count
405                count = 0;
406                break;
407        }
408        final TextView countView = (TextView) view.findViewById(R.id.message_count);
409
410        // Set folder icon
411        final ImageView folderIcon = (ImageView) view.findViewById(R.id.folder_icon);
412        folderIcon.setImageDrawable(
413                FolderProperties.getInstance(context).getIcon(type, id, flags));
414
415        final ImageView mailboxExpandedIcon =
416                (ImageView) view.findViewById(R.id.folder_expanded_icon);
417        switch (rowType) {
418            case ROW_TYPE_SUBMAILBOX:
419                if (hasVisibleChildren) {
420                    mailboxExpandedIcon.setVisibility(View.VISIBLE);
421                    mailboxExpandedIcon.setImageResource(
422                            R.drawable.ic_mailbox_collapsed_holo_light);
423                } else {
424                    mailboxExpandedIcon.setVisibility(View.INVISIBLE);
425                    mailboxExpandedIcon.setImageDrawable(null);
426                }
427                folderIcon.setVisibility(View.INVISIBLE);
428                break;
429            case ROW_TYPE_CURMAILBOX:
430                mailboxExpandedIcon.setVisibility(View.GONE);
431                mailboxExpandedIcon.setImageDrawable(null);
432                folderIcon.setVisibility(View.GONE);
433                break;
434            case ROW_TYPE_MAILBOX:
435            default: // Includes ROW_TYPE_ACCOUNT
436                if (hasVisibleChildren) {
437                    mailboxExpandedIcon.setVisibility(View.VISIBLE);
438                    mailboxExpandedIcon.setImageResource(
439                            R.drawable.ic_mailbox_collapsed_holo_light);
440                } else {
441                    mailboxExpandedIcon.setVisibility(View.GONE);
442                    mailboxExpandedIcon.setImageDrawable(null);
443                }
444                folderIcon.setVisibility(View.VISIBLE);
445                break;
446        }
447
448        // If the unread count is zero, not to show countView.
449        if (count > 0) {
450            countView.setVisibility(View.VISIBLE);
451            countView.setText(Integer.toString(count));
452        } else {
453            countView.setVisibility(View.GONE);
454        }
455
456        final View chipView = view.findViewById(R.id.color_chip);
457        if (isAccount) {
458            chipView.setVisibility(View.VISIBLE);
459            chipView.setBackgroundColor(mResourceHelper.getAccountColor(id));
460        } else {
461            chipView.setVisibility(View.GONE);
462        }
463    }
464
465    /**
466     * Returns a cursor loader for the mailboxes of the given account.  If <code>parentKey</code>
467     * refers to a valid mailbox ID [e.g. non-zero], restrict the loader to only those mailboxes
468     * contained by this parent mailbox.
469     *
470     * Note the returned loader always returns a {@link CursorWithExtras}.
471     */
472    static Loader<Cursor> createMailboxesLoader(Context context, long accountId,
473            long parentMailboxId) {
474        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
475            Log.d(Logging.LOG_TAG, "MailboxFragmentAdapter#CursorWithExtras accountId=" + accountId
476                    + " parentMailboxId=" + parentMailboxId);
477        }
478        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
479            throw new IllegalArgumentException();
480        }
481        return new MailboxFragmentLoader(context, accountId, parentMailboxId);
482    }
483
484    /**
485     * Returns a cursor loader for the combined view.
486     */
487    static Loader<Cursor> createCombinedViewLoader(Context context) {
488        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
489            Log.d(Logging.LOG_TAG, "MailboxFragmentAdapter#createCombinedViewLoader");
490        }
491        return new CombinedMailboxLoader(context);
492    }
493
494    /**
495     * Adds a new row into the given cursor.
496     */
497    private static void addMailboxRow(MatrixCursor cursor, long mailboxId, String displayName,
498            int mailboxType, int unreadCount, int messageCount, int rowType, int flags,
499            long accountId) {
500        long listId = mailboxId;
501        if (mailboxId < 0) {
502            listId = Long.MAX_VALUE + mailboxId; // IDs for the list view must be positive
503        }
504        RowBuilder row = cursor.newRow();
505        row.add(listId);
506        row.add(mailboxId);
507        row.add(displayName);
508        row.add(mailboxType);
509        row.add(unreadCount);
510        row.add(messageCount);
511        row.add(rowType);
512        row.add(flags);
513        row.add(accountId);
514    }
515
516    private static void addCombinedMailboxRow(MatrixCursor cursor, long id, int mailboxType,
517            int count, boolean showAlways) {
518        if (id >= 0) {
519            throw new IllegalArgumentException(); // Must be QUERY_ALL_*, which are all negative
520        }
521        if (showAlways || (count > 0)) {
522            addMailboxRow(
523                    cursor, id, "", mailboxType, count, count, ROW_TYPE_MAILBOX, Mailbox.FLAG_NONE,
524                    Account.ACCOUNT_ID_COMBINED_VIEW);
525        }
526    }
527
528    /**
529     * Loads mailboxes that are the children of a given mailbox ID.
530     *
531     * The returned {@link Cursor} is always a {@link CursorWithExtras}.
532     */
533    private static class MailboxFragmentLoader extends ThrottlingCursorLoader {
534        private final Context mContext;
535        private final long mAccountId;
536        private final long mParentKey;
537
538        MailboxFragmentLoader(Context context, long accountId, long parentKey) {
539            super(context, Mailbox.CONTENT_URI,
540                    (parentKey != Mailbox.NO_MAILBOX)
541                            ? SUBMAILBOX_PROJECTION
542                            : PROJECTION,
543                    USER_MAILBOX_SELECTION_WITH_PARENT,
544                    new String[] { Long.toString(accountId), Long.toString(parentKey) },
545                    MAILBOX_ORDER_BY);
546            mContext = context;
547            mAccountId = accountId;
548            mParentKey = parentKey;
549        }
550
551        @Override
552        public void onContentChanged() {
553            if (sEnableUpdate) {
554                super.onContentChanged();
555            }
556        }
557
558        @Override
559        public Cursor loadInBackground() {
560            boolean parentRemoved = false;
561
562            final Cursor userMailboxCursor = super.loadInBackground();
563            final Cursor returnCursor;
564
565            final int childCount = userMailboxCursor.getCount();
566
567            if (mParentKey != Mailbox.NO_MAILBOX) {
568                // If we're not showing the top level mailboxes, add the "parent" mailbox.
569                final Cursor parentCursor = getContext().getContentResolver().query(
570                        Mailbox.CONTENT_URI, CURMAILBOX_PROJECTION, MAILBOX_SELECTION,
571                        new String[] { Long.toString(mAccountId), Long.toString(mParentKey) },
572                        null);
573                returnCursor = new MergeCursor(new Cursor[] { parentCursor, userMailboxCursor });
574            } else {
575                // TODO Add per-account starred mailbox support
576                final MatrixCursor starredCursor = new MatrixCursor(MATRIX_PROJECTION);
577                final Cursor systemMailboxCursor = mContext.getContentResolver().query(
578                        Mailbox.CONTENT_URI, PROJECTION, SYSTEM_MAILBOX_SELECTION,
579                        new String[] { Long.toString(mAccountId) }, MAILBOX_ORDER_BY);
580                final MatrixCursor recentCursor = new MatrixCursor(MATRIX_PROJECTION);
581                final MatrixCursor headerCursor = new MatrixCursor(MATRIX_PROJECTION);
582                if (childCount > 0) {
583                    final String name = mContext.getString(R.string.mailbox_list_user_mailboxes);
584                    addMailboxRow(headerCursor, 0L, name, 0, 0, 0, ROW_TYPE_HEADER, 0, 0L);
585                }
586                ArrayList<Long> recentList = null;
587                boolean useTwoPane = UiUtilities.useTwoPane(mContext);
588                if (useTwoPane) {
589                    recentList = RecentMailboxManager.getInstance(mContext)
590                            .getMostRecent(mAccountId, true);
591                }
592                if (recentList != null && recentList.size() > 0) {
593                    final String name = mContext.getString(R.string.mailbox_list_recent_mailboxes);
594                    addMailboxRow(recentCursor, 0L, name, 0, 0, 0, ROW_TYPE_HEADER, 0, 0L);
595                    for (long mailboxId : recentList) {
596                        final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
597                        if (mailbox == null) continue;
598                        final int messageCount = Utility.getFirstRowInt(mContext,
599                            ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
600                            new String[] { MailboxColumns.MESSAGE_COUNT }, null, null, null, 0);
601                        final int unreadCount = Utility.getFirstRowInt(mContext,
602                            ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
603                            new String[] { MailboxColumns.UNREAD_COUNT }, null, null, null, 0);
604                        addMailboxRow(recentCursor, mailboxId, mailbox.mDisplayName, mailbox.mType,
605                            unreadCount, messageCount, ROW_TYPE_MAILBOX, mailbox.mFlags,
606                            mailbox.mAccountKey);
607                    }
608                }
609                int accountStarredCount = Message.getFavoriteMessageCount(mContext, mAccountId);
610                if (accountStarredCount > 0) {
611                    // Only add "Starred", if there is at least one starred message
612                    final int totalStarredCount = Message.getFavoriteMessageCount(mContext);
613                    addCombinedMailboxRow(starredCursor, Mailbox.QUERY_ALL_FAVORITES,
614                            Mailbox.TYPE_MAIL, totalStarredCount, true);
615                }
616                returnCursor = new MergeCursor(new Cursor[] {
617                        starredCursor, systemMailboxCursor, recentCursor, headerCursor,
618                        userMailboxCursor, });
619            }
620            return new CursorWithExtras(returnCursor, childCount);
621        }
622    }
623
624    /**
625     * Loader for mailboxes in "Combined view".
626     */
627    @VisibleForTesting
628    static class CombinedMailboxLoader extends ThrottlingCursorLoader {
629        private static final String[] ACCOUNT_PROJECTION = new String[] {
630            EmailContent.RECORD_ID, AccountColumns.DISPLAY_NAME,
631        };
632        private static final int COLUMN_ACCOUND_ID = 0;
633        private static final int COLUMN_ACCOUNT_DISPLAY_NAME = 1;
634
635        private final Context mContext;
636
637        private CombinedMailboxLoader(Context context) {
638            super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null, null);
639            mContext = context;
640        }
641
642        @Override
643        public Cursor loadInBackground() {
644            final Cursor accounts = super.loadInBackground();
645
646            // Build combined mailbox rows.
647            final MatrixCursor returnCursor = buildCombinedMailboxes(mContext, accounts);
648
649            // Add account rows.
650            accounts.moveToPosition(-1);
651            while (accounts.moveToNext()) {
652                final long accountId = accounts.getLong(COLUMN_ACCOUND_ID);
653                final String accountName = accounts.getString(COLUMN_ACCOUNT_DISPLAY_NAME);
654                final int unreadCount = Mailbox.getUnreadCountByAccountAndMailboxType(
655                        mContext, accountId, Mailbox.TYPE_INBOX);
656                addMailboxRow(returnCursor, accountId, accountName, Mailbox.TYPE_NONE,
657                        unreadCount, unreadCount, ROW_TYPE_ACCOUNT, Mailbox.FLAG_NONE,
658                        accountId);
659            }
660            return returnCursor;
661        }
662
663        @VisibleForTesting
664        static MatrixCursor buildCombinedMailboxes(Context context, Cursor innerCursor) {
665            MatrixCursor cursor = new ClosingMatrixCursor(MATRIX_PROJECTION, innerCursor);
666            // Combined inbox -- show unread count
667            addCombinedMailboxRow(cursor, Mailbox.QUERY_ALL_INBOXES, Mailbox.TYPE_INBOX,
668                    Mailbox.getUnreadCountByMailboxType(context, Mailbox.TYPE_INBOX), true);
669
670            // Favorite (starred) -- show # of favorites
671            addCombinedMailboxRow(cursor, Mailbox.QUERY_ALL_FAVORITES, Mailbox.TYPE_MAIL,
672                    Message.getFavoriteMessageCount(context), false);
673
674            // Drafts -- show # of drafts
675            addCombinedMailboxRow(cursor, Mailbox.QUERY_ALL_DRAFTS, Mailbox.TYPE_DRAFTS,
676                    Mailbox.getMessageCountByMailboxType(context, Mailbox.TYPE_DRAFTS), false);
677
678            // Outbox -- # of outstanding messages
679            addCombinedMailboxRow(cursor, Mailbox.QUERY_ALL_OUTBOX, Mailbox.TYPE_OUTBOX,
680                    Mailbox.getMessageCountByMailboxType(context, Mailbox.TYPE_OUTBOX), false);
681
682            return cursor;
683        }
684    }
685}
686