AccountSelectorAdapter.java revision c75638981761bfc9cb1c042bdd3dcc4d66e17eb9
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.ContentResolver;
20import android.content.ContentUris;
21import android.content.Context;
22import android.content.Loader;
23import android.database.Cursor;
24import android.database.MatrixCursor;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.AdapterView;
29import android.widget.CursorAdapter;
30import android.widget.TextView;
31
32import com.android.email.FolderProperties;
33import com.android.email.R;
34import com.android.email.ResourceHelper;
35import com.android.email.data.ClosingMatrixCursor;
36import com.android.email.data.ThrottlingCursorLoader;
37import com.android.emailcommon.provider.Account;
38import com.android.emailcommon.provider.EmailContent;
39import com.android.emailcommon.provider.EmailContent.AccountColumns;
40import com.android.emailcommon.provider.EmailContent.MailboxColumns;
41import com.android.emailcommon.provider.Mailbox;
42import com.android.emailcommon.utility.Utility;
43import com.google.common.annotations.VisibleForTesting;
44import com.google.common.base.Preconditions;
45
46import java.util.ArrayList;
47import java.util.Collection;
48
49/**
50 * Account selector spinner.
51 *
52 * TODO Test it!
53 */
54public class AccountSelectorAdapter extends CursorAdapter {
55    /** meta data column for an message count (unread or total, depending on row) */
56    private static final String MESSAGE_COUNT = "unreadCount";
57
58    /** meta data column for the row type; used for display purposes */
59    private static final String ROW_TYPE = "rowType";
60
61    /** meta data position of the currently selected account in the drop-down list */
62    private static final String ACCOUNT_POSITION = "accountPosition";
63
64    /** "account id" virtual column name for the matrix cursor */
65    private static final String ACCOUNT_ID = "accountId";
66
67    private static final int ROW_TYPE_HEADER = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
68    @SuppressWarnings("unused")
69    private static final int ROW_TYPE_MAILBOX = 0;
70    private static final int ROW_TYPE_ACCOUNT = 1;
71    private static final int ITEM_VIEW_TYPE_ACCOUNT = 0;
72    static final int UNKNOWN_POSITION = -1;
73    /** Projection for account database query */
74    private static final String[] ACCOUNT_PROJECTION = new String[] {
75        EmailContent.RECORD_ID,
76        Account.DISPLAY_NAME,
77        Account.EMAIL_ADDRESS,
78    };
79    /**
80     * Projection used for the selector display; we add meta data that doesn't exist in the
81     * account database, so, this should be a super-set of {@link #ACCOUNT_PROJECTION}.
82     */
83    private static final String[] ADAPTER_PROJECTION = new String[] {
84        ROW_TYPE,
85        EmailContent.RECORD_ID,
86        Account.DISPLAY_NAME,
87        Account.EMAIL_ADDRESS,
88        MESSAGE_COUNT,
89        ACCOUNT_POSITION, // TODO Probably we don't really need this
90        ACCOUNT_ID,
91    };
92
93    /** Sort order.  Show the default account first. */
94    private static final String ORDER_BY = Account.IS_DEFAULT + " desc, " + Account.RECORD_ID;
95
96    @SuppressWarnings("hiding")
97    private final Context mContext;
98    private final LayoutInflater mInflater;
99    private final ResourceHelper mResourceHelper;
100
101    /**
102     * Returns a loader that can populate the account spinner.
103     * @param context a context
104     * @param accountId the ID of the currently viewed account
105     */
106    public static Loader<Cursor> createLoader(Context context, long accountId, long mailboxId) {
107        return new AccountsLoader(context, accountId, mailboxId, UiUtilities.useTwoPane(context));
108    }
109
110    public AccountSelectorAdapter(Context context) {
111        super(context, null, 0 /* no auto-requery */);
112        mContext = context;
113        mInflater = LayoutInflater.from(context);
114        mResourceHelper = ResourceHelper.getInstance(context);
115    }
116
117    /**
118     * {@inheritDoc}
119     *
120     * The account selector view can contain one of four types of row data:
121     * <ol>
122     * <li>headers</li>
123     * <li>accounts</li>
124     * <li>recent mailboxes</li>
125     * <li>"show all folders"</li>
126     * </ol>
127     * Headers are handled separately as they have a unique layout and cannot be interacted with.
128     * Accounts, recent mailboxes and "show all folders" all have the same interaction model and
129     * share a very similar layout. The single difference is that both accounts and recent
130     * mailboxes display an unread count; whereas "show all folders" does not. To determine
131     * if a particular row is "show all folders" verify that a) it's not an account row and
132     * b) it's ID is {@link Mailbox#NO_MAILBOX}.
133     *
134     * TODO Use recycled views.  ({@link #getViewTypeCount} and {@link #getItemViewType})
135     */
136    @Override
137    public View getView(int position, View convertView, ViewGroup parent) {
138        Cursor c = getCursor();
139        c.moveToPosition(position);
140        View view;
141        if (c.getInt(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER) {
142            view = mInflater.inflate(R.layout.action_bar_spinner_dropdown_header, parent, false);
143            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
144            final String displayName = getDisplayName(c);
145            displayNameView.setText(displayName);
146        } else {
147            view = mInflater.inflate(R.layout.action_bar_spinner_dropdown, parent, false);
148            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
149            final TextView emailAddressView = (TextView) view.findViewById(R.id.email_address);
150            final TextView unreadCountView = (TextView) view.findViewById(R.id.unread_count);
151            final View chipView = view.findViewById(R.id.color_chip);
152
153            final String displayName = getDisplayName(c);
154            final String emailAddress = getAccountEmailAddress(c);
155
156            displayNameView.setText(displayName);
157
158            // Show the email address only when it's different from the display name.
159            boolean isAccount = isAccountItem(c);
160            if (displayName.equals(emailAddress) || !isAccount) {
161                emailAddressView.setVisibility(View.GONE);
162            } else {
163                emailAddressView.setVisibility(View.VISIBLE);
164                emailAddressView.setText(emailAddress);
165            }
166
167            long id = getId(c);
168            if (isAccount || id != Mailbox.NO_MAILBOX) {
169                unreadCountView.setVisibility(View.VISIBLE);
170                unreadCountView.setText(UiUtilities.getMessageCountForUi(mContext,
171                        getAccountUnreadCount(c), true));
172
173                // If we're on a combined account, show the color chip indicators for all real
174                // accounts so it can be used as a legend.
175                boolean isCombinedActive =
176                        ((CursorWithExtras) c).getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW;
177
178                if (isCombinedActive && Account.isNormalAccount(id)) {
179                    chipView.setBackgroundColor(mResourceHelper.getAccountColor(id));
180                    chipView.setVisibility(View.VISIBLE);
181                } else {
182                    chipView.setVisibility(View.GONE);
183                }
184            } else {
185                unreadCountView.setVisibility(View.INVISIBLE);
186                chipView.setVisibility(View.GONE);
187            }
188
189        }
190        return view;
191    }
192
193    @Override
194    public View newView(Context context, Cursor cursor, ViewGroup parent) {
195        return null; // we don't reuse views.  This method never gets called.
196    }
197
198    @Override
199    public void bindView(View view, Context context, Cursor cursor) {
200        // we don't reuse views.  This method never gets called.
201    }
202
203    @Override
204    public int getViewTypeCount() {
205        return 2;
206    }
207
208    @Override
209    public int getItemViewType(int position) {
210        Cursor c = getCursor();
211        c.moveToPosition(position);
212        return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER
213                ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
214                : ITEM_VIEW_TYPE_ACCOUNT;
215    }
216
217    @Override
218    public boolean isEnabled(int position) {
219        return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER);
220    }
221
222    public boolean isAccountItem(int position) {
223        Cursor c = getCursor();
224        c.moveToPosition(position);
225        return isAccountItem(c);
226    }
227
228    public boolean isAccountItem(Cursor c) {
229        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_ACCOUNT);
230    }
231
232    public boolean isMailboxItem(int position) {
233        Cursor c = getCursor();
234        c.moveToPosition(position);
235        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_MAILBOX);
236    }
237
238    private int getAccountUnreadCount(Cursor c) {
239        return getMessageCount(c);
240    }
241
242    /**
243     * Returns the account/mailbox ID extracted from the given cursor.
244     */
245    private static long getId(Cursor c) {
246        return c.getLong(c.getColumnIndex(EmailContent.RECORD_ID));
247    }
248
249    /**
250     * @return ID of the account / mailbox for a row
251     */
252    public long getId(int position) {
253        final Cursor c = getCursor();
254        return c.moveToPosition(position) ? getId(c) : Account.NO_ACCOUNT;
255    }
256
257    /**
258     * @return ID of the account for a row
259     */
260    public long getAccountId(int position) {
261        final Cursor c = getCursor();
262        return c.moveToPosition(position)
263                ? c.getLong(c.getColumnIndex(ACCOUNT_ID))
264                : Account.NO_ACCOUNT;
265    }
266
267    /** Returns the account name extracted from the given cursor. */
268    static String getDisplayName(Cursor cursor) {
269        return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME));
270    }
271
272    /** Returns the email address extracted from the given cursor. */
273    private static String getAccountEmailAddress(Cursor cursor) {
274        return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS));
275    }
276
277    /**
278     * Returns the message count (unread or total, depending on row) extracted from the given
279     * cursor.
280     */
281    private static int getMessageCount(Cursor cursor) {
282        return cursor.getInt(cursor.getColumnIndex(MESSAGE_COUNT));
283    }
284
285    private static String sCombinedViewDisplayName;
286    private static String getCombinedViewDisplayName(Context c) {
287        if (sCombinedViewDisplayName == null) {
288            sCombinedViewDisplayName = c.getResources().getString(
289                    R.string.mailbox_list_account_selector_combined_view);
290        }
291        return sCombinedViewDisplayName;
292    }
293
294    /**
295     * Load the account list.  The resulting cursor contains
296     * - Account info
297     * - # of unread messages in inbox
298     * - The "Combined view" row if there's more than one account.
299     */
300    @VisibleForTesting
301    static class AccountsLoader extends ThrottlingCursorLoader {
302        private final Context mContext;
303        private final long mAccountId;
304        private final long mMailboxId;
305        private final boolean mUseTwoPane; // Injectable for test
306        private final FolderProperties mFolderProperties;
307
308        @VisibleForTesting
309        AccountsLoader(Context context, long accountId, long mailboxId, boolean useTwoPane) {
310            // Super class loads a regular account cursor, but we replace it in loadInBackground().
311            super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null,
312                    ORDER_BY);
313            mContext = context;
314            mAccountId = accountId;
315            mMailboxId = mailboxId;
316            mFolderProperties = FolderProperties.getInstance(mContext);
317            mUseTwoPane = useTwoPane;
318        }
319
320        @Override
321        public Cursor loadInBackground() {
322            final Cursor accountsCursor = super.loadInBackground();
323            // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed.
324            final CursorWithExtras resultCursor
325                    = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor);
326            final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor);
327            addMailboxesToCursor(resultCursor, accountPosition);
328
329            resultCursor.setAccountMailboxInfo(getContext(), mAccountId, mMailboxId);
330            return resultCursor;
331        }
332
333        /** Adds the account list [with extra meta data] to the given matrix cursor */
334        private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) {
335            int accountPosition = UNKNOWN_POSITION;
336            accountCursor.moveToPosition(-1);
337
338            matrixCursor.mAccountCount = accountCursor.getCount();
339            int totalUnread = 0;
340            while (accountCursor.moveToNext()) {
341                // Add account, with its unread count.
342                final long accountId = accountCursor.getLong(0);
343                final int unread = Mailbox.getUnreadCountByAccountAndMailboxType(
344                        mContext, accountId, Mailbox.TYPE_INBOX);
345                final String name = getDisplayName(accountCursor);
346                final String emailAddress = getAccountEmailAddress(accountCursor);
347                addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread,
348                    UNKNOWN_POSITION, accountId);
349                totalUnread += unread;
350                if (accountId == mAccountId) {
351                    accountPosition = accountCursor.getPosition();
352                }
353            }
354            // Add "combined view" if more than one account exists
355            final int countAccounts = accountCursor.getCount();
356            if (countAccounts > 1) {
357                final String accountCount = mContext.getResources().getQuantityString(
358                        R.plurals.number_of_accounts, countAccounts, countAccounts);
359                addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW,
360                        getCombinedViewDisplayName(mContext),
361                        accountCount, totalUnread, UNKNOWN_POSITION,
362                        Account.ACCOUNT_ID_COMBINED_VIEW);
363
364                // Increment the account count for the combined account.
365                matrixCursor.mAccountCount++;
366            }
367            return accountPosition;
368        }
369
370        /**
371         * Adds the recent mailbox list / "show all folders" to the given cursor.
372         *
373         * @param matrixCursor the cursor to add the list to
374         * @param accountPosition the cursor position of the currently selected account
375         */
376        private void addMailboxesToCursor(CursorWithExtras matrixCursor, int accountPosition) {
377            if (mAccountId == Account.NO_ACCOUNT) {
378                return; // Account not selected
379            }
380            if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
381                if (!mUseTwoPane) {
382                    // TODO We may want a header for this to separate it from the account list
383                    addShowAllFoldersRow(matrixCursor, accountPosition);
384                }
385                return;
386            }
387            String emailAddress = null;
388            if (accountPosition != UNKNOWN_POSITION) {
389                matrixCursor.moveToPosition(accountPosition);
390                emailAddress =
391                        matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS));
392            }
393            RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext);
394            ArrayList<Long> recentMailboxes = null;
395            if (!mUseTwoPane) {
396                // Do not display recent mailboxes in the account spinner for the two pane view
397                recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane);
398            }
399            final int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size();
400            matrixCursor.mRecentCount = recentCount;
401
402            if (!mUseTwoPane) {
403                // "Recent mailboxes" header
404                addHeaderRow(matrixCursor, mContext.getString(
405                        R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress));
406            }
407
408            if (recentCount > 0) {
409                addMailboxRows(matrixCursor, accountPosition, recentMailboxes);
410            }
411
412            if (!mUseTwoPane) {
413                addShowAllFoldersRow(matrixCursor, accountPosition);
414            }
415        }
416
417        private void addShowAllFoldersRow(CursorWithExtras matrixCursor, int accountPosition) {
418            matrixCursor.mHasShowAllFolders = true;
419            String name = mContext.getString(
420                    R.string.mailbox_list_account_selector_show_all_folders);
421            addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0,
422                    accountPosition, mAccountId);
423        }
424
425
426        private static final String[] RECENT_MAILBOX_INFO_PROJECTION = new String[] {
427            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
428            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
429        };
430
431        private void addMailboxRows(MatrixCursor matrixCursor, int accountPosition,
432                Collection<Long> mailboxIds) {
433            Cursor c = mContext.getContentResolver().query(
434                    Mailbox.CONTENT_URI, RECENT_MAILBOX_INFO_PROJECTION,
435                    Utility.buildInSelection(MailboxColumns.ID, mailboxIds), null,
436                    RecentMailboxManager.RECENT_MAILBOXES_SORT_ORDER);
437            try {
438                c.moveToPosition(-1);
439                while (c.moveToNext()) {
440                    addRow(matrixCursor, ROW_TYPE_MAILBOX,
441                            c.getLong(c.getColumnIndex(MailboxColumns.ID)),
442                            mFolderProperties.getDisplayName(c), null,
443                            mFolderProperties.getMessageCount(c), accountPosition, mAccountId);
444                }
445            } finally {
446                c.close();
447            }
448        }
449
450        private void addHeaderRow(MatrixCursor cursor, String name) {
451            addRow(cursor, ROW_TYPE_HEADER, 0L, name, null, 0, UNKNOWN_POSITION,
452                    Account.NO_ACCOUNT);
453        }
454
455        /** Adds a row to the given cursor */
456        private void addRow(MatrixCursor cursor, int rowType, long id, String name,
457                String emailAddress, int messageCount, int listPosition, long accountId) {
458            cursor.newRow()
459                .add(rowType)
460                .add(id)
461                .add(name)
462                .add(emailAddress)
463                .add(messageCount)
464                .add(listPosition)
465                .add(accountId);
466        }
467    }
468
469    /** Cursor with some extra meta data. */
470    static class CursorWithExtras extends ClosingMatrixCursor {
471
472        /** Number of account elements, including the combined account row. */
473        private int mAccountCount;
474        /** Number of recent mailbox elements */
475        private int mRecentCount;
476        private boolean mHasShowAllFolders;
477
478        private boolean mAccountExists;
479
480        /**
481         * Account ID that's loaded.
482         */
483        private long mAccountId;
484        private String mAccountDisplayName;
485
486        /**
487         * Mailbox ID that's loaded.
488         */
489        private long mMailboxId;
490        private String mMailboxDisplayName;
491        private int mMailboxMessageCount;
492
493        @VisibleForTesting
494        CursorWithExtras(String[] columnNames, Cursor innerCursor) {
495            super(columnNames, innerCursor);
496        }
497
498        private static final String[] ACCOUNT_INFO_PROJECTION = new String[] {
499            AccountColumns.DISPLAY_NAME,
500        };
501        private static final String[] MAILBOX_INFO_PROJECTION = new String[] {
502            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
503            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
504        };
505
506        /**
507         * Set the current account/mailbox info.
508         */
509        @VisibleForTesting
510        void setAccountMailboxInfo(Context context, long accountId, long mailboxId) {
511            mAccountId = accountId;
512            mMailboxId = mailboxId;
513
514            // Get account info
515            if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
516                // We need to treat ACCOUNT_ID_COMBINED_VIEW specially...
517                mAccountExists = true;
518                mAccountDisplayName = getCombinedViewDisplayName(context);
519                if (mailboxId != Mailbox.NO_MAILBOX) {
520                    setCombinedMailboxInfo(context, mailboxId);
521                }
522                return;
523            }
524
525            mAccountDisplayName = Utility.getFirstRowString(context,
526                    ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
527                    ACCOUNT_INFO_PROJECTION, null, null, null, 0, null);
528            if (mAccountDisplayName == null) {
529                // Account gone!
530                mAccountExists = false;
531                return;
532            }
533            mAccountExists = true;
534
535            // If mailbox not specified, done.
536            if (mMailboxId == Mailbox.NO_MAILBOX) {
537                return;
538            }
539            // Combined mailbox?
540            // Unfortunately this can happen even when account != ACCOUNT_ID_COMBINED_VIEW,
541            // when you open "starred" on 2-pane on non-combined view.
542            if (mMailboxId < 0) {
543                setCombinedMailboxInfo(context, mailboxId);
544                return;
545            }
546
547            // Get mailbox info
548            final ContentResolver r = context.getContentResolver();
549            final Cursor mailboxCursor = r.query(
550                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
551                    MAILBOX_INFO_PROJECTION, null, null, null);
552            try {
553                if (mailboxCursor.moveToFirst()) {
554                    final FolderProperties fp = FolderProperties.getInstance(context);
555                    mMailboxDisplayName = fp.getDisplayName(mailboxCursor);
556                    mMailboxMessageCount = fp.getMessageCount(mailboxCursor);
557                }
558            } finally {
559                mailboxCursor.close();
560            }
561        }
562
563        private void setCombinedMailboxInfo(Context context, long mailboxId) {
564            Preconditions.checkState(mailboxId < -1, "Not combined mailbox");
565            mMailboxDisplayName = FolderProperties.getInstance(context)
566                    .getCombinedMailboxName(mMailboxId);
567
568            mMailboxMessageCount = FolderProperties.getMessageCountForCombinedMailbox(
569                    context, mailboxId);
570        }
571
572        /**
573         * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION}
574         * if the given ID does not exist.
575         */
576        int getPosition(long id) {
577            moveToPosition(-1);
578            while(moveToNext()) {
579                if (id == getId(this)) {
580                    return getPosition();
581                }
582            }
583            return UNKNOWN_POSITION;
584        }
585
586        public int getAccountCount() {
587            return mAccountCount;
588        }
589
590        @VisibleForTesting
591        public int getRecentMailboxCount() {
592            return mRecentCount;
593        }
594
595        /**
596         * @return true if the cursor has more than one selectable item so we should enable the
597         *     spinner.
598         */
599        public boolean shouldEnableSpinner() {
600            return mHasShowAllFolders || (mAccountCount + mRecentCount > 1);
601        }
602
603        public long getAccountId() {
604            return mAccountId;
605        }
606
607        public String getAccountDisplayName() {
608            return mAccountDisplayName;
609        }
610
611        @VisibleForTesting
612        public long getMailboxId() {
613            return mMailboxId;
614        }
615
616        public String getMailboxDisplayName() {
617            return mMailboxDisplayName;
618        }
619
620        public int getMailboxMessageCount() {
621            return mMailboxMessageCount;
622        }
623
624        /**
625         * @return {@code true} if the specified accuont exists.
626         */
627        public boolean accountExists() {
628            return mAccountExists;
629        }
630    }
631}
632