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