AccountSelectorAdapter.java revision 0ec545048a66e3af465a0f1c28d13435886dbfc1
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            boolean isAccount = isAccountItem(position);
166            long id = getAccountId(c);
167            if (isAccount || id != Mailbox.NO_MAILBOX) {
168                unreadCountView.setVisibility(View.VISIBLE);
169                unreadCountView.setText(UiUtilities.getMessageCountForUi(mContext,
170                        getAccountUnreadCount(position), false));
171            } else {
172                unreadCountView.setVisibility(View.INVISIBLE);
173            }
174        }
175        return view;
176    }
177
178    @Override
179    public void bindView(View view, Context context, Cursor cursor) {
180        TextView textView = (TextView) view.findViewById(R.id.display_name);
181        textView.setText(getAccountDisplayName(cursor));
182    }
183
184    @Override
185    public View newView(Context context, Cursor cursor, ViewGroup parent) {
186        return mInflater.inflate(R.layout.account_selector, parent, false);
187    }
188
189    @Override
190    public int getViewTypeCount() {
191        return 2;
192    }
193
194    @Override
195    public int getItemViewType(int position) {
196        Cursor c = getCursor();
197        c.moveToPosition(position);
198        return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER
199                ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
200                : ITEM_VIEW_TYPE_ACCOUNT;
201    }
202
203    @Override
204    public boolean isEnabled(int position) {
205        return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER);
206    }
207
208    public boolean isAccountItem(int position) {
209        Cursor c = getCursor();
210        c.moveToPosition(position);
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 String getAccountDisplayName(int position) {
221        final Cursor c = getCursor();
222        return c.moveToPosition(position) ? getAccountDisplayName(c) : null;
223    }
224
225    private String getAccountEmailAddress(int position) {
226        final Cursor c = getCursor();
227        return c.moveToPosition(position) ? getAccountEmailAddress(c) : null;
228    }
229
230    private int getAccountUnreadCount(int position) {
231        final Cursor c = getCursor();
232        return c.moveToPosition(position) ? getAccountUnreadCount(c) : 0;
233    }
234
235    int getAccountPosition(int position) {
236        final Cursor c = getCursor();
237        return c.moveToPosition(position) ? getAccountPosition(c) : UNKNOWN_POSITION;
238    }
239
240    /** Returns the account ID extracted from the given cursor. */
241    private static long getAccountId(Cursor c) {
242        return c.getLong(c.getColumnIndex(Account.ID));
243    }
244
245    /** Returns the account name extracted from the given cursor. */
246    static String getAccountDisplayName(Cursor cursor) {
247        return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME));
248    }
249
250    /** Returns the email address extracted from the given cursor. */
251    private static String getAccountEmailAddress(Cursor cursor) {
252        return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS));
253    }
254
255    /** Returns the unread count extracted from the given cursor. */
256    private static int getAccountUnreadCount(Cursor cursor) {
257        return cursor.getInt(cursor.getColumnIndex(UNREAD_COUNT));
258    }
259
260    /** Returns the account position extracted from the given cursor. */
261    private static int getAccountPosition(Cursor cursor) {
262        return cursor.getInt(cursor.getColumnIndex(ACCOUNT_POSITION));
263    }
264
265    /**
266     * Load the account list.  The resulting cursor contains
267     * - Account info
268     * - # of unread messages in inbox
269     * - The "Combined view" row if there's more than one account.
270     */
271    @VisibleForTesting
272    static class AccountsLoader extends ThrottlingCursorLoader {
273        private final Context mContext;
274        private final long mAccountId;
275        public AccountsLoader(Context context, long accountId) {
276            // Super class loads a regular account cursor, but we replace it in loadInBackground().
277            super(context, EmailContent.Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null,
278                    ORDER_BY);
279            mContext = context;
280            mAccountId = accountId;
281        }
282
283        @Override
284        public Cursor loadInBackground() {
285            final Cursor accountsCursor = super.loadInBackground();
286            // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed.
287            final CursorWithExtras resultCursor
288                    = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor);
289            final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor);
290            addRecentsToCursor(resultCursor, accountPosition);
291            return Utility.CloseTraceCursorWrapper.get(resultCursor);
292        }
293
294        /** Adds the account list [with extra meta data] to the given matrix cursor */
295        private int addAccountsToCursor(MatrixCursor matrixCursor, Cursor accountCursor) {
296            int accountPosition = UNKNOWN_POSITION;
297            accountCursor.moveToPosition(-1);
298            // Add a header for the accounts
299            String header =
300                    mContext.getString(R.string.mailbox_list_account_selector_account_header);
301            addRow(matrixCursor, ROW_TYPE_HEADER, 0L, header, null, 0, UNKNOWN_POSITION);
302            int totalUnread = 0;
303            int currentPosition = 1;
304            while (accountCursor.moveToNext()) {
305                // Add account, with its unread count.
306                final long accountId = accountCursor.getLong(0);
307                final int unread = Mailbox.getUnreadCountByAccountAndMailboxType(
308                        mContext, accountId, Mailbox.TYPE_INBOX);
309                final String name = getAccountDisplayName(accountCursor);
310                final String emailAddress = getAccountEmailAddress(accountCursor);
311                addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread,
312                    UNKNOWN_POSITION);
313                totalUnread += unread;
314                if (accountId == mAccountId) {
315                    accountPosition = currentPosition;
316                }
317                currentPosition++;
318            }
319            // Add "combined view" if more than one account exists
320            final int countAccounts = accountCursor.getCount();
321            if (countAccounts > 1) {
322                final String name = mContext.getResources().getString(
323                        R.string.mailbox_list_account_selector_combined_view);
324                final String accountCount = mContext.getResources().getQuantityString(
325                        R.plurals.number_of_accounts, countAccounts, countAccounts);
326                addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW,
327                        name, accountCount, totalUnread, UNKNOWN_POSITION);
328            }
329            return accountPosition;
330        }
331
332        /**
333         * Adds the recent mailbox list to the given cursor.
334         * @param matrixCursor the cursor to add the list to
335         * @param accountPosition the cursor position of the currently selected account
336         */
337        private void addRecentsToCursor(CursorWithExtras matrixCursor, int accountPosition) {
338            if (mAccountId <= 0L || mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
339                // Currently selected account isn't usable for our purposes
340                return;
341            }
342            String emailAddress = null;
343            if (accountPosition != UNKNOWN_POSITION) {
344                matrixCursor.moveToPosition(accountPosition);
345                emailAddress =
346                        matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS));
347            }
348            RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext);
349            ArrayList<Long> recentMailboxes = null;
350            boolean useTwoPane = UiUtilities.useTwoPane(mContext);
351            if (!useTwoPane) {
352                // Do not display recent mailboxes in the account spinner for the two pane view
353                recentMailboxes = mailboxManager.getMostRecent(mAccountId, useTwoPane);
354            }
355            int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size();
356            matrixCursor.mRecentCount = recentCount;
357            if (recentCount > 0) {
358                String mailboxHeader = mContext.getString(
359                    R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress);
360                addRow(matrixCursor, ROW_TYPE_HEADER, 0L, mailboxHeader, null, 0, UNKNOWN_POSITION);
361                for (long mailboxId : recentMailboxes) {
362                    final int unread = Utility.getFirstRowInt(mContext,
363                            ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
364                            new String[] { MailboxColumns.UNREAD_COUNT }, null, null, null, 0);
365                    final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
366                    addRow(matrixCursor, ROW_TYPE_MAILBOX, mailboxId, mailbox.mDisplayName, null,
367                            unread, accountPosition);
368                }
369            } else if (!useTwoPane) {
370                // Add the header for 'show all folders'
371                String mailboxHeader = mContext.getString(
372                    R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress);
373                addRow(matrixCursor, ROW_TYPE_HEADER, 0L, mailboxHeader, null, 0, UNKNOWN_POSITION);
374            }
375            if (!useTwoPane) {
376                String name = mContext.getString(
377                        R.string.mailbox_list_account_selector_show_all_folders);
378                addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0,
379                        accountPosition);
380            }
381        }
382
383        /** Adds a row to the given cursor */
384        private void addRow(MatrixCursor cursor, int rowType, long id, String name,
385                String emailAddress, int unreadCount, int listPosition) {
386            cursor.newRow()
387                .add(rowType)
388                .add(id)
389                .add(name)
390                .add(emailAddress)
391                .add(unreadCount)
392                .add(listPosition);
393        }
394    }
395
396    /** Cursor with some extra meta data. */
397    static class CursorWithExtras extends ClosingMatrixCursor {
398        /** Number of account elements */
399        final int mAccountCount;
400        /** Number of recent mailbox elements */
401        int mRecentCount;
402
403        private CursorWithExtras(String[] columnNames, Cursor innerCursor) {
404            super(columnNames, innerCursor);
405            mAccountCount = (innerCursor == null) ? 0 : innerCursor.getCount();
406        }
407
408        /**
409         * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION}
410         * if the given ID does not exist.
411         */
412        int getPosition(long id) {
413            moveToPosition(-1);
414            while(moveToNext()) {
415                if (id == getAccountId(this)) {
416                    return getPosition();
417                }
418            }
419            return UNKNOWN_POSITION;
420        }
421    }
422}
423