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