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