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