AccountSelectorAdapter.java revision 22cc33158d1336fe39b44046bc584c0f1c4838f5
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            if (displayName.equals(emailAddress)) {
160                emailAddressView.setVisibility(View.GONE);
161            } else {
162                emailAddressView.setVisibility(View.VISIBLE);
163                emailAddressView.setText(emailAddress);
164            }
165
166            boolean isAccount = isAccountItem(c);
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 (((CursorWithExtras) c).getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW) {
174                    chipView.setBackgroundColor(mResourceHelper.getAccountColor(id));
175                    chipView.setVisibility(View.VISIBLE);
176                } else {
177                    chipView.setVisibility(View.GONE);
178                }
179            } else {
180                unreadCountView.setVisibility(View.INVISIBLE);
181                chipView.setVisibility(View.GONE);
182            }
183
184        }
185        return view;
186    }
187
188    @Override
189    public View newView(Context context, Cursor cursor, ViewGroup parent) {
190        return null; // we don't reuse views.  This method never gets called.
191    }
192
193    @Override
194    public void bindView(View view, Context context, Cursor cursor) {
195        // we don't reuse views.  This method never gets called.
196    }
197
198    @Override
199    public int getViewTypeCount() {
200        return 2;
201    }
202
203    @Override
204    public int getItemViewType(int position) {
205        Cursor c = getCursor();
206        c.moveToPosition(position);
207        return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER
208                ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
209                : ITEM_VIEW_TYPE_ACCOUNT;
210    }
211
212    @Override
213    public boolean isEnabled(int position) {
214        return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER);
215    }
216
217    public boolean isAccountItem(int position) {
218        Cursor c = getCursor();
219        c.moveToPosition(position);
220        return isAccountItem(c);
221    }
222
223    public boolean isAccountItem(Cursor c) {
224        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_ACCOUNT);
225    }
226
227    public boolean isMailboxItem(int position) {
228        Cursor c = getCursor();
229        c.moveToPosition(position);
230        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_MAILBOX);
231    }
232
233    private int getAccountUnreadCount(Cursor c) {
234        return getMessageCount(c);
235    }
236
237    /**
238     * Returns the account/mailbox ID extracted from the given cursor.
239     */
240    private static long getId(Cursor c) {
241        return c.getLong(c.getColumnIndex(EmailContent.RECORD_ID));
242    }
243
244    /**
245     * @return ID of the account / mailbox for a row
246     */
247    public long getId(int position) {
248        final Cursor c = getCursor();
249        return c.moveToPosition(position) ? getId(c) : Account.NO_ACCOUNT;
250    }
251
252    /**
253     * @return ID of the account for a row
254     */
255    public long getAccountId(int position) {
256        final Cursor c = getCursor();
257        return c.moveToPosition(position)
258                ? c.getLong(c.getColumnIndex(ACCOUNT_ID))
259                : Account.NO_ACCOUNT;
260    }
261
262    /** Returns the account name extracted from the given cursor. */
263    static String getDisplayName(Cursor cursor) {
264        return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME));
265    }
266
267    /** Returns the email address extracted from the given cursor. */
268    private static String getAccountEmailAddress(Cursor cursor) {
269        return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS));
270    }
271
272    /**
273     * Returns the message count (unread or total, depending on row) extracted from the given
274     * cursor.
275     */
276    private static int getMessageCount(Cursor cursor) {
277        return cursor.getInt(cursor.getColumnIndex(MESSAGE_COUNT));
278    }
279
280    private static String sCombinedViewDisplayName;
281    private static String getCombinedViewDisplayName(Context c) {
282        if (sCombinedViewDisplayName == null) {
283            sCombinedViewDisplayName = c.getResources().getString(
284                    R.string.mailbox_list_account_selector_combined_view);
285        }
286        return sCombinedViewDisplayName;
287    }
288
289    /**
290     * Load the account list.  The resulting cursor contains
291     * - Account info
292     * - # of unread messages in inbox
293     * - The "Combined view" row if there's more than one account.
294     */
295    @VisibleForTesting
296    static class AccountsLoader extends ThrottlingCursorLoader {
297        private final Context mContext;
298        private final long mAccountId;
299        private final long mMailboxId;
300        private final boolean mUseTwoPane; // Injectable for test
301        private final FolderProperties mFolderProperties;
302
303        @VisibleForTesting
304        AccountsLoader(Context context, long accountId, long mailboxId, boolean useTwoPane) {
305            // Super class loads a regular account cursor, but we replace it in loadInBackground().
306            super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null,
307                    ORDER_BY);
308            mContext = context;
309            mAccountId = accountId;
310            mMailboxId = mailboxId;
311            mFolderProperties = FolderProperties.getInstance(mContext);
312            mUseTwoPane = useTwoPane;
313        }
314
315        @Override
316        public Cursor loadInBackground() {
317            final Cursor accountsCursor = super.loadInBackground();
318            // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed.
319            final CursorWithExtras resultCursor
320                    = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor);
321            final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor);
322            addMailboxesToCursor(resultCursor, accountPosition);
323
324            resultCursor.setAccountMailboxInfo(getContext(), mAccountId, mMailboxId);
325            return resultCursor;
326        }
327
328        /** Adds the account list [with extra meta data] to the given matrix cursor */
329        private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) {
330            int accountPosition = UNKNOWN_POSITION;
331            accountCursor.moveToPosition(-1);
332
333            matrixCursor.mAccountCount = accountCursor.getCount();
334            int totalUnread = 0;
335            int currentPosition = 1;
336            while (accountCursor.moveToNext()) {
337                // Add account, with its unread count.
338                final long accountId = accountCursor.getLong(0);
339                final int unread = Mailbox.getUnreadCountByAccountAndMailboxType(
340                        mContext, accountId, Mailbox.TYPE_INBOX);
341                final String name = getDisplayName(accountCursor);
342                final String emailAddress = getAccountEmailAddress(accountCursor);
343                addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread,
344                    UNKNOWN_POSITION, accountId);
345                totalUnread += unread;
346                if (accountId == mAccountId) {
347                    accountPosition = currentPosition;
348                }
349                currentPosition++;
350            }
351            // Add "combined view" if more than one account exists
352            final int countAccounts = accountCursor.getCount();
353            if (countAccounts > 1) {
354                final String accountCount = mContext.getResources().getQuantityString(
355                        R.plurals.number_of_accounts, countAccounts, countAccounts);
356                addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW,
357                        getCombinedViewDisplayName(mContext),
358                        accountCount, totalUnread, UNKNOWN_POSITION,
359                        Account.ACCOUNT_ID_COMBINED_VIEW);
360
361                // Increment the account count for the combined account.
362                matrixCursor.mAccountCount++;
363            }
364            return accountPosition;
365        }
366
367        /**
368         * Adds the recent mailbox list / "show all folders" to the given cursor.
369         *
370         * @param matrixCursor the cursor to add the list to
371         * @param accountPosition the cursor position of the currently selected account
372         */
373        private void addMailboxesToCursor(CursorWithExtras matrixCursor, int accountPosition) {
374            if (mAccountId == Account.NO_ACCOUNT) {
375                return; // Account not selected
376            }
377            if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
378                if (!mUseTwoPane) {
379                    // TODO We may want a header for this to separate it from the account list
380                    addShowAllFoldersRow(matrixCursor, accountPosition);
381                }
382                return;
383            }
384            String emailAddress = null;
385            if (accountPosition != UNKNOWN_POSITION) {
386                matrixCursor.moveToPosition(accountPosition);
387                emailAddress =
388                        matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS));
389            }
390            RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext);
391            ArrayList<Long> recentMailboxes = null;
392            if (!mUseTwoPane) {
393                // Do not display recent mailboxes in the account spinner for the two pane view
394                recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane);
395            }
396            final int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size();
397            matrixCursor.mRecentCount = recentCount;
398
399            if (!mUseTwoPane) {
400                // "Recent mailboxes" header
401                addHeaderRow(matrixCursor, mContext.getString(
402                        R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress));
403            }
404
405            if (recentCount > 0) {
406                addMailboxRows(matrixCursor, accountPosition, recentMailboxes);
407            }
408
409            if (!mUseTwoPane) {
410                addShowAllFoldersRow(matrixCursor, accountPosition);
411            }
412        }
413
414        private void addShowAllFoldersRow(CursorWithExtras matrixCursor, int accountPosition) {
415            matrixCursor.mHasShowAllFolders = true;
416            String name = mContext.getString(
417                    R.string.mailbox_list_account_selector_show_all_folders);
418            addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0,
419                    accountPosition, mAccountId);
420        }
421
422
423        private static final String[] RECENT_MAILBOX_INFO_PROJECTION = new String[] {
424            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
425            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
426        };
427
428        private void addMailboxRows(MatrixCursor matrixCursor, int accountPosition,
429                Collection<Long> mailboxIds) {
430            Cursor c = mContext.getContentResolver().query(
431                    Mailbox.CONTENT_URI, RECENT_MAILBOX_INFO_PROJECTION,
432                    Utility.buildInSelection(MailboxColumns.ID, mailboxIds), null,
433                    RecentMailboxManager.RECENT_MAILBOXES_SORT_ORDER);
434            try {
435                c.moveToPosition(-1);
436                while (c.moveToNext()) {
437                    addRow(matrixCursor, ROW_TYPE_MAILBOX,
438                            c.getLong(c.getColumnIndex(MailboxColumns.ID)),
439                            mFolderProperties.getDisplayName(c), null,
440                            mFolderProperties.getMessageCount(c), accountPosition, mAccountId);
441                }
442            } finally {
443                c.close();
444            }
445        }
446
447        private void addHeaderRow(MatrixCursor cursor, String name) {
448            addRow(cursor, ROW_TYPE_HEADER, 0L, name, null, 0, UNKNOWN_POSITION,
449                    Account.NO_ACCOUNT);
450        }
451
452        /** Adds a row to the given cursor */
453        private void addRow(MatrixCursor cursor, int rowType, long id, String name,
454                String emailAddress, int messageCount, int listPosition, long accountId) {
455            cursor.newRow()
456                .add(rowType)
457                .add(id)
458                .add(name)
459                .add(emailAddress)
460                .add(messageCount)
461                .add(listPosition)
462                .add(accountId);
463        }
464    }
465
466    /** Cursor with some extra meta data. */
467    static class CursorWithExtras extends ClosingMatrixCursor {
468
469        /** Number of account elements, including the combined account row. */
470        private int mAccountCount;
471        /** Number of recent mailbox elements */
472        private int mRecentCount;
473        private boolean mHasShowAllFolders;
474
475        private boolean mAccountExists;
476
477        /**
478         * Account ID that's loaded.
479         */
480        private long mAccountId;
481        private String mAccountDisplayName;
482
483        /**
484         * Mailbox ID that's loaded.
485         */
486        private long mMailboxId;
487        private String mMailboxDisplayName;
488        private int mMailboxMessageCount;
489
490        @VisibleForTesting
491        CursorWithExtras(String[] columnNames, Cursor innerCursor) {
492            super(columnNames, innerCursor);
493        }
494
495        private static final String[] ACCOUNT_INFO_PROJECTION = new String[] {
496            AccountColumns.DISPLAY_NAME,
497        };
498        private static final String[] MAILBOX_INFO_PROJECTION = new String[] {
499            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
500            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
501        };
502
503        /**
504         * Set the current account/mailbox info.
505         */
506        @VisibleForTesting
507        void setAccountMailboxInfo(Context context, long accountId, long mailboxId) {
508            mAccountId = accountId;
509            mMailboxId = mailboxId;
510
511            // Get account info
512            if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
513                // We need to treat ACCOUNT_ID_COMBINED_VIEW specially...
514                mAccountExists = true;
515                mAccountDisplayName = getCombinedViewDisplayName(context);
516                if (mailboxId != Mailbox.NO_MAILBOX) {
517                    setCombinedMailboxInfo(context, mailboxId);
518                }
519                return;
520            }
521
522            mAccountDisplayName = Utility.getFirstRowString(context,
523                    ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
524                    ACCOUNT_INFO_PROJECTION, null, null, null, 0, null);
525            if (mAccountDisplayName == null) {
526                // Account gone!
527                mAccountExists = false;
528                return;
529            }
530            mAccountExists = true;
531
532            // If mailbox not specified, done.
533            if (mMailboxId == Mailbox.NO_MAILBOX) {
534                return;
535            }
536            // Combined mailbox?
537            // Unfortunately this can happen even when account != ACCOUNT_ID_COMBINED_VIEW,
538            // when you open "starred" on 2-pane on non-combined view.
539            if (mMailboxId < 0) {
540                setCombinedMailboxInfo(context, mailboxId);
541                return;
542            }
543
544            // Get mailbox info
545            final ContentResolver r = context.getContentResolver();
546            final Cursor mailboxCursor = r.query(
547                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
548                    MAILBOX_INFO_PROJECTION, null, null, null);
549            try {
550                if (mailboxCursor.moveToFirst()) {
551                    final FolderProperties fp = FolderProperties.getInstance(context);
552                    mMailboxDisplayName = fp.getDisplayName(mailboxCursor);
553                    mMailboxMessageCount = fp.getMessageCount(mailboxCursor);
554                }
555            } finally {
556                mailboxCursor.close();
557            }
558        }
559
560        private void setCombinedMailboxInfo(Context context, long mailboxId) {
561            Preconditions.checkState(mailboxId < -1, "Not combined mailbox");
562            mMailboxDisplayName = FolderProperties.getInstance(context)
563                    .getCombinedMailboxName(mMailboxId);
564
565            mMailboxMessageCount = FolderProperties.getMessageCountForCombinedMailbox(
566                    context, mailboxId);
567        }
568
569        /**
570         * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION}
571         * if the given ID does not exist.
572         */
573        int getPosition(long id) {
574            moveToPosition(-1);
575            while(moveToNext()) {
576                if (id == getId(this)) {
577                    return getPosition();
578                }
579            }
580            return UNKNOWN_POSITION;
581        }
582
583        public int getAccountCount() {
584            return mAccountCount;
585        }
586
587        @VisibleForTesting
588        public int getRecentMailboxCount() {
589            return mRecentCount;
590        }
591
592        /**
593         * @return true if the cursor has more than one selectable item so we should enable the
594         *     spinner.
595         */
596        public boolean shouldEnableSpinner() {
597            return mHasShowAllFolders || (mAccountCount + mRecentCount > 1);
598        }
599
600        public long getAccountId() {
601            return mAccountId;
602        }
603
604        public String getAccountDisplayName() {
605            return mAccountDisplayName;
606        }
607
608        @VisibleForTesting
609        public long getMailboxId() {
610            return mMailboxId;
611        }
612
613        public String getMailboxDisplayName() {
614            return mMailboxDisplayName;
615        }
616
617        public int getMailboxMessageCount() {
618            return mMailboxMessageCount;
619        }
620
621        /**
622         * @return {@code true} if the specified accuont exists.
623         */
624        public boolean accountExists() {
625            return mAccountExists;
626        }
627    }
628}
629