AccountSelectorAdapter.java revision 80d3875d306c60da83e547c573427627911f8a99
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.MailboxColumns;
28import com.android.emailcommon.provider.Mailbox;
29import com.android.emailcommon.utility.Utility;
30
31import java.util.ArrayList;
32
33import android.content.ContentUris;
34import android.content.Context;
35import android.content.Loader;
36import android.database.Cursor;
37import android.database.MatrixCursor;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.AdapterView;
42import android.widget.CursorAdapter;
43import android.widget.TextView;
44
45/**
46 * Account selector spinner.
47 *
48 * TODO Test it!
49 */
50public class AccountSelectorAdapter extends CursorAdapter {
51    /** meta data column for an message count (unread or total, depending on row) */
52    private static final String MESSAGE_COUNT = "unreadCount";
53    /** meta data column for the row type; used for display purposes */
54    private static final String ROW_TYPE = "rowType";
55    /** meta data position of the currently selected account in the drop-down list */
56    private static final String ACCOUNT_POSITION = "accountPosition";
57    private static final int ROW_TYPE_HEADER = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
58    @SuppressWarnings("unused")
59    private static final int ROW_TYPE_MAILBOX = 0;
60    private static final int ROW_TYPE_ACCOUNT = 1;
61    private static final int ITEM_VIEW_TYPE_ACCOUNT = 0;
62    static final int UNKNOWN_POSITION = -1;
63    /** Projection for account database query */
64    private static final String[] ACCOUNT_PROJECTION = new String[] {
65        EmailContent.RECORD_ID,
66        Account.DISPLAY_NAME,
67        Account.EMAIL_ADDRESS,
68    };
69    /**
70     * Projection used for the selector display; we add meta data that doesn't exist in the
71     * account database, so, this should be a super-set of {@link #ACCOUNT_PROJECTION}.
72     */
73    private static final String[] ADAPTER_PROJECTION = new String[] {
74        ROW_TYPE,
75        EmailContent.RECORD_ID,
76        Account.DISPLAY_NAME,
77        Account.EMAIL_ADDRESS,
78        MESSAGE_COUNT,
79        ACCOUNT_POSITION,
80    };
81
82    /** Sort order.  Show the default account first. */
83    private static final String ORDER_BY =
84            Account.IS_DEFAULT + " desc, " + Account.RECORD_ID;
85
86    private final LayoutInflater mInflater;
87    @SuppressWarnings("hiding")
88    private final Context mContext;
89
90    /**
91     * Returns a loader that can populate the account spinner.
92     * @param context a context
93     * @param accountId the ID of the currently viewed account
94     */
95    public static Loader<Cursor> createLoader(Context context, long accountId) {
96        return new AccountsLoader(context, accountId, UiUtilities.useTwoPane(context));
97    }
98
99    public AccountSelectorAdapter(Context context) {
100        super(context, null, 0 /* no auto-requery */);
101        mContext = context;
102        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
103    }
104
105    /**
106     * Invoked when the action bar needs the view of the text in the bar itself. The default
107     * is to show just the display name of the cursor at the given position.
108     */
109    @Override
110    public View getView(int position, View convertView, ViewGroup parent) {
111        if (!isAccountItem(position)) {
112            // asked to show a recent mailbox; instead, show the account associated w/ the mailbox
113            int newPosition = getAccountPosition(position);
114            if (newPosition != UNKNOWN_POSITION) {
115                position = newPosition;
116            }
117        }
118        return super.getView(position, convertView, parent);
119    }
120
121    /**
122     * The account selector view can contain one of four types of row data:
123     * <ol>
124     * <li>headers</li>
125     * <li>accounts</li>
126     * <li>recent mailboxes</li>
127     * <li>"show all folders"</li>
128     * </ol>
129     * Headers are handled separately as they have a unique layout and cannot be interacted with.
130     * Accounts, recent mailboxes and "show all folders" all have the same interaction model and
131     * share a very similar layout. The single difference is that both accounts and recent
132     * mailboxes display an unread count; whereas "show all folders" does not. To determine
133     * if a particular row is "show all folders" verify that a) it's not an account row and
134     * b) it's ID is {@link Mailbox#NO_MAILBOX}.
135     */
136    @Override
137    public View getDropDownView(int position, View convertView, ViewGroup parent) {
138        Cursor c = getCursor();
139        c.moveToPosition(position);
140
141        View view;
142        if (c.getInt(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER) {
143            view = mInflater.inflate(R.layout.account_selector_dropdown_header, parent, false);
144            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
145            final String displayName = getDisplayName(c);
146            displayNameView.setText(displayName);
147        } else {
148            view = mInflater.inflate(R.layout.account_selector_dropdown, parent, false);
149            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
150            final TextView emailAddressView = (TextView) view.findViewById(R.id.email_address);
151            final TextView unreadCountView = (TextView) view.findViewById(R.id.unread_count);
152
153            final String displayName = getDisplayName(position);
154            final String emailAddress = getAccountEmailAddress(position);
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(position);
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(position), true));
172            } else {
173                unreadCountView.setVisibility(View.INVISIBLE);
174            }
175        }
176        return view;
177    }
178
179    @Override
180    public void bindView(View view, Context context, Cursor cursor) {
181        TextView textView = (TextView) view.findViewById(R.id.display_name);
182        textView.setText(getDisplayName(cursor));
183    }
184
185    @Override
186    public View newView(Context context, Cursor cursor, ViewGroup parent) {
187        return mInflater.inflate(R.layout.account_selector, parent, false);
188    }
189
190    @Override
191    public int getViewTypeCount() {
192        return 2;
193    }
194
195    @Override
196    public int getItemViewType(int position) {
197        Cursor c = getCursor();
198        c.moveToPosition(position);
199        return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER
200                ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
201                : ITEM_VIEW_TYPE_ACCOUNT;
202    }
203
204    @Override
205    public boolean isEnabled(int position) {
206        return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER);
207    }
208
209    public boolean isAccountItem(int position) {
210        Cursor c = getCursor();
211        c.moveToPosition(position);
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 String getDisplayName(int position) {
222        final Cursor c = getCursor();
223        return c.moveToPosition(position) ? getDisplayName(c) : null;
224    }
225
226    private String getAccountEmailAddress(int position) {
227        final Cursor c = getCursor();
228        return c.moveToPosition(position) ? getAccountEmailAddress(c) : null;
229    }
230
231    private int getAccountUnreadCount(int position) {
232        final Cursor c = getCursor();
233        return c.moveToPosition(position) ? getMessageCount(c) : 0;
234    }
235
236    int getAccountPosition(int position) {
237        final Cursor c = getCursor();
238        return c.moveToPosition(position) ? getAccountPosition(c) : UNKNOWN_POSITION;
239    }
240
241    /**
242     * Returns the account/mailbox ID extracted from the given cursor.
243     */
244    private static long getId(Cursor c) {
245        return c.getLong(c.getColumnIndex(EmailContent.RECORD_ID));
246    }
247
248    /** Returns the account name extracted from the given cursor. */
249    static String getDisplayName(Cursor cursor) {
250        return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME));
251    }
252
253    /** Returns the email address extracted from the given cursor. */
254    private static String getAccountEmailAddress(Cursor cursor) {
255        return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS));
256    }
257
258    /**
259     * Returns the message count (unread or total, depending on row) extracted from the given
260     * cursor.
261     */
262    private static int getMessageCount(Cursor cursor) {
263        return cursor.getInt(cursor.getColumnIndex(MESSAGE_COUNT));
264    }
265
266    /** Returns the account position extracted from the given cursor. */
267    private static int getAccountPosition(Cursor cursor) {
268        return cursor.getInt(cursor.getColumnIndex(ACCOUNT_POSITION));
269    }
270
271    /**
272     * Load the account list.  The resulting cursor contains
273     * - Account info
274     * - # of unread messages in inbox
275     * - The "Combined view" row if there's more than one account.
276     */
277    @VisibleForTesting
278    static class AccountsLoader extends ThrottlingCursorLoader {
279        private final Context mContext;
280        private final long mAccountId;
281        private final boolean mUseTwoPane; // Injectable for test
282        private final FolderProperties mFolderProperties;
283
284        @VisibleForTesting
285        AccountsLoader(Context context, long accountId, boolean useTwoPane) {
286            // Super class loads a regular account cursor, but we replace it in loadInBackground().
287            super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null,
288                    ORDER_BY);
289            mContext = context;
290            mAccountId = accountId;
291            mFolderProperties = FolderProperties.getInstance(mContext);
292            mUseTwoPane = useTwoPane;
293        }
294
295        @Override
296        public Cursor loadInBackground() {
297            final Cursor accountsCursor = super.loadInBackground();
298            // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed.
299            final CursorWithExtras resultCursor
300                    = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor);
301            final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor);
302            addRecentsToCursor(resultCursor, accountPosition);
303            return Utility.CloseTraceCursorWrapper.get(resultCursor);
304        }
305
306        /** Adds the account list [with extra meta data] to the given matrix cursor */
307        private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) {
308            int accountPosition = UNKNOWN_POSITION;
309            accountCursor.moveToPosition(-1);
310            // Add a header for the accounts
311            String header =
312                    mContext.getString(R.string.mailbox_list_account_selector_account_header);
313            addRow(matrixCursor, ROW_TYPE_HEADER, 0L, header, null, 0, UNKNOWN_POSITION);
314
315            matrixCursor.mAccountCount = accountCursor.getCount();
316            int totalUnread = 0;
317            int currentPosition = 1;
318            while (accountCursor.moveToNext()) {
319                // Add account, with its unread count.
320                final long accountId = accountCursor.getLong(0);
321                final int unread = Mailbox.getUnreadCountByAccountAndMailboxType(
322                        mContext, accountId, Mailbox.TYPE_INBOX);
323                final String name = getDisplayName(accountCursor);
324                final String emailAddress = getAccountEmailAddress(accountCursor);
325                addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread,
326                    UNKNOWN_POSITION);
327                totalUnread += unread;
328                if (accountId == mAccountId) {
329                    accountPosition = currentPosition;
330                }
331                currentPosition++;
332            }
333            // Add "combined view" if more than one account exists
334            final int countAccounts = accountCursor.getCount();
335            if (countAccounts > 1) {
336                final String name = mContext.getResources().getString(
337                        R.string.mailbox_list_account_selector_combined_view);
338                final String accountCount = mContext.getResources().getQuantityString(
339                        R.plurals.number_of_accounts, countAccounts, countAccounts);
340                addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW,
341                        name, accountCount, totalUnread, UNKNOWN_POSITION);
342
343                // Increment the account count for the combined account.
344                matrixCursor.mAccountCount++;
345            }
346            return accountPosition;
347        }
348
349        /**
350         * Adds the recent mailbox list to the given cursor.
351         * @param matrixCursor the cursor to add the list to
352         * @param accountPosition the cursor position of the currently selected account
353         */
354        private void addRecentsToCursor(CursorWithExtras matrixCursor, int accountPosition) {
355            if (mAccountId <= 0L || mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
356                // Currently selected account isn't usable for our purposes
357                return;
358            }
359            String emailAddress = null;
360            if (accountPosition != UNKNOWN_POSITION) {
361                matrixCursor.moveToPosition(accountPosition);
362                emailAddress =
363                        matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS));
364            }
365            RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext);
366            ArrayList<Long> recentMailboxes = null;
367            if (!mUseTwoPane) {
368                // Do not display recent mailboxes in the account spinner for the two pane view
369                recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane);
370            }
371            int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size();
372            matrixCursor.mRecentCount = recentCount;
373
374            if (!mUseTwoPane) {
375                // "Recent mailboxes" header
376                String mailboxHeader = mContext.getString(
377                        R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress);
378                addRow(matrixCursor, ROW_TYPE_HEADER, 0L, mailboxHeader, null, 0, UNKNOWN_POSITION);
379            }
380
381            if (recentCount > 0) {
382                for (long mailboxId : recentMailboxes) {
383                    addMailboxRow(matrixCursor, accountPosition, mailboxId);
384                }
385            }
386
387            if (!mUseTwoPane) {
388                // TODO We need this for combined view too.
389                String name = mContext.getString(
390                        R.string.mailbox_list_account_selector_show_all_folders);
391                addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0,
392                        accountPosition);
393            }
394        }
395
396
397        private static final String[] RECENT_MAILBOX_INFO_PROJECTION = new String[] {
398            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
399            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
400        };
401
402        private void addMailboxRow(MatrixCursor matrixCursor, int accountPosition, long mailboxId) {
403            Cursor c = mContext.getContentResolver().query(
404                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
405                    RECENT_MAILBOX_INFO_PROJECTION, null, null, null);
406            if (!c.moveToFirst()) {
407                return;
408            }
409            addRow(matrixCursor, ROW_TYPE_MAILBOX, mailboxId,
410                    mFolderProperties.getDisplayName(c), null,
411                    mFolderProperties.getMessageCount(c), accountPosition);
412        }
413
414        /** Adds a row to the given cursor */
415        private void addRow(MatrixCursor cursor, int rowType, long id, String name,
416                String emailAddress, int messageCount, int listPosition) {
417            cursor.newRow()
418                .add(rowType)
419                .add(id)
420                .add(name)
421                .add(emailAddress)
422                .add(messageCount)
423                .add(listPosition);
424        }
425    }
426
427    /** Cursor with some extra meta data. */
428    static class CursorWithExtras extends ClosingMatrixCursor {
429        /** Number of account elements, including the combined account row. */
430        private int mAccountCount;
431        /** Number of recent mailbox elements */
432        private int mRecentCount;
433
434        private CursorWithExtras(String[] columnNames, Cursor innerCursor) {
435            super(columnNames, innerCursor);
436        }
437
438        /**
439         * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION}
440         * if the given ID does not exist.
441         */
442        int getPosition(long id) {
443            moveToPosition(-1);
444            while(moveToNext()) {
445                if (id == getId(this)) {
446                    return getPosition();
447                }
448            }
449            return UNKNOWN_POSITION;
450        }
451
452        public int getAccountCount() {
453            return mAccountCount;
454        }
455
456        public int getRecentMailboxCount() {
457            return mRecentCount;
458        }
459    }
460}
461