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