ShortcutPickerFragment.java revision f47255f6fcdcf94c55c580b1784af1cfa3f2f7e3
1/*
2 * Copyright (C) 2011 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.android.email.R;
20import com.android.emailcommon.provider.EmailContent.Account;
21import com.android.emailcommon.provider.EmailContent.AccountColumns;
22import com.android.emailcommon.provider.EmailContent.MailboxColumns;
23import com.android.emailcommon.provider.HostAuth;
24import com.android.emailcommon.provider.Mailbox;
25
26import android.app.Activity;
27import android.app.ListFragment;
28import android.app.LoaderManager.LoaderCallbacks;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.CursorLoader;
32import android.content.Loader;
33import android.content.res.Resources;
34import android.database.Cursor;
35import android.database.MatrixCursor;
36import android.database.MergeCursor;
37import android.database.MatrixCursor.RowBuilder;
38import android.net.Uri;
39import android.os.Bundle;
40import android.view.View;
41import android.widget.AdapterView;
42import android.widget.ListView;
43import android.widget.SimpleCursorAdapter;
44import android.widget.AdapterView.OnItemClickListener;
45
46/**
47 * Fragment containing a list of accounts to show during shortcut creation.
48 * <p>
49 * NOTE: In order to receive callbacks, the activity containing this fragment must implement
50 * the {@link PickerCallback} interface.
51 */
52public abstract class ShortcutPickerFragment extends ListFragment
53        implements OnItemClickListener, LoaderCallbacks<Cursor> {
54    /** Allow all mailboxes in the mailbox list */
55    public static int FILTER_ALLOW_ALL    = 0;
56    /** Only allow an account's INBOX */
57    public static int FILTER_INBOX_ONLY   = 1 << 0;
58    /** Allow an "unread" mailbox; this is not affected by {@link #FILTER_INBOX_ONLY} */
59    public static int FILTER_ALLOW_UNREAD = 1 << 1;
60    /** Fragment argument to set filter values */
61    public static final String ARG_FILTER  = "ShortcutPickerFragment.filter";
62    /** The filter values; default to allow all mailboxes */
63    private Integer mFilter;
64    /** Callback methods. Enclosing activities must implement to receive fragment notifications. */
65    public static interface PickerCallback {
66        /** Invoked when an account and mailbox have been selected. */
67        public void onSelected(Account account, long mailboxId);
68        /** Required data is missing; either the account and/or mailbox */
69        public void onMissingData(boolean missingAccount, boolean missingMailbox);
70    }
71
72    /** A no-op callback */
73    private final PickerCallback EMPTY_CALLBACK = new PickerCallback() {
74        @Override public void onSelected(Account account, long mailboxId){ getActivity().finish(); }
75        @Override public void onMissingData(boolean missingAccount, boolean missingMailbox) { }
76    };
77    private final static int LOADER_ID = 0;
78    private final static int[] TO_VIEWS = new int[] {
79        android.R.id.text1,
80    };
81
82    PickerCallback mCallback = EMPTY_CALLBACK;
83    /** Cursor adapter that provides either the account or mailbox list */
84    private SimpleCursorAdapter mAdapter;
85
86    @Override
87    public void onAttach(Activity activity) {
88        super.onAttach(activity);
89
90        if (activity instanceof PickerCallback) {
91            mCallback = (PickerCallback) activity;
92        }
93        final String[] fromColumns = getFromColumns();
94        mAdapter = new SimpleCursorAdapter(activity,
95            android.R.layout.simple_expandable_list_item_1, null, fromColumns, TO_VIEWS, 0);
96        setListAdapter(mAdapter);
97
98        getLoaderManager().initLoader(LOADER_ID, null, this);
99    }
100
101    @Override
102    public void onActivityCreated(Bundle savedInstanceState) {
103        super.onActivityCreated(savedInstanceState);
104
105        ListView listView = getListView();
106        listView.setOnItemClickListener(this);
107        listView.setItemsCanFocus(false);
108    }
109
110    @Override
111    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
112        mAdapter.swapCursor(data);
113    }
114
115    @Override
116    public void onLoaderReset(Loader<Cursor> loader) {
117        mAdapter.swapCursor(null);
118    }
119
120    /** Returns the cursor columns to map into list */
121    abstract String[] getFromColumns();
122
123    /** Returns the mailbox filter */
124    int getFilter() {
125        if (mFilter == null) {
126            Bundle args = getArguments();
127            if (args != null) {
128                mFilter = args.getInt(ARG_FILTER, FILTER_ALLOW_ALL);
129            } else {
130                // No arguments set on fragment, use a default value
131                mFilter = FILTER_ALLOW_ALL;
132            }
133        }
134        return mFilter;
135    }
136
137    // TODO if we add meta-accounts to the database, remove this class entirely
138    private static final class AccountPickerLoader extends CursorLoader {
139        public AccountPickerLoader(Context context, Uri uri, String[] projection, String selection,
140                String[] selectionArgs, String sortOrder) {
141            super(context, uri, projection, selection, selectionArgs, sortOrder);
142        }
143
144        @Override
145        public Cursor loadInBackground() {
146            Cursor parentCursor = super.loadInBackground();
147            int cursorCount = parentCursor.getCount();
148            final Cursor returnCursor;
149
150            if (cursorCount > 1) {
151                // Only add "All accounts" if there is more than 1 account defined
152                MatrixCursor allAccountCursor = new MatrixCursor(getProjection());
153                addCombinedAccountRow(allAccountCursor, cursorCount);
154                returnCursor = new MergeCursor(new Cursor[] { allAccountCursor, parentCursor });
155            } else {
156                returnCursor = parentCursor;
157            }
158            return returnCursor;
159        }
160
161        /** Adds a row for "All Accounts" into the given cursor */
162        private void addCombinedAccountRow(MatrixCursor cursor, int accountCount) {
163            Context context = getContext();
164            Account account = new Account();
165            account.mId = Account.ACCOUNT_ID_COMBINED_VIEW;
166            Resources res = context.getResources();
167            String countString = res.getQuantityString(R.plurals.picker_combined_view_account_count,
168                    accountCount, accountCount);
169            account.mDisplayName = res.getString(R.string.picker_combined_view_fmt, countString);
170            ContentValues values = account.toContentValues();
171            RowBuilder row = cursor.newRow();
172            for (String rowName : cursor.getColumnNames()) {
173                // special case some of the rows ...
174                if (AccountColumns.ID.equals(rowName)) {
175                    row.add(Account.ACCOUNT_ID_COMBINED_VIEW);
176                    continue;
177                } else if (AccountColumns.IS_DEFAULT.equals(rowName)) {
178                    row.add(0);
179                    continue;
180                }
181                row.add(values.get(rowName));
182            }
183        }
184    }
185
186    /** Account picker */
187    public static class AccountShortcutPickerFragment extends ShortcutPickerFragment {
188        private final static String[] ACCOUNT_FROM_COLUMNS = new String[] {
189            AccountColumns.DISPLAY_NAME,
190        };
191
192        @Override
193        public void onActivityCreated(Bundle savedInstanceState) {
194            super.onActivityCreated(savedInstanceState);
195            getActivity().setTitle(R.string.account_shortcut_picker_title);
196        }
197
198        @Override
199        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
200            Cursor cursor = (Cursor) parent.getItemAtPosition(position);
201            selectAccountCursor(cursor);
202        }
203
204        @Override
205        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
206            Context context = getActivity();
207            return new AccountPickerLoader(
208                context, Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null, null);
209        }
210
211        @Override
212        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
213            // if there is only one account, auto-select it
214            // No accounts; close the dialog
215            if (data.getCount() == 0) {
216                mCallback.onMissingData(true, false);
217                return;
218            }
219            if (data.getCount() == 1 && data.moveToFirst()) {
220                selectAccountCursor(data);
221                return;
222            }
223            super.onLoadFinished(loader, data);
224        }
225
226        @Override
227        String[] getFromColumns() {
228            return ACCOUNT_FROM_COLUMNS;
229        }
230
231        /** Selects the account specified by the given cursor */
232        private void selectAccountCursor(Cursor cursor) {
233            Account account = new Account();
234            account.restore(cursor);
235            ShortcutPickerFragment fragment = new MailboxShortcutPickerFragment();
236            final Bundle args = new Bundle();
237            args.putParcelable(MailboxShortcutPickerFragment.ARG_ACCOUNT, account);
238            args.putInt(ARG_FILTER, getFilter());
239            fragment.setArguments(args);
240            getFragmentManager()
241                .beginTransaction()
242                    .replace(R.id.shortcut_list, fragment)
243                    .addToBackStack(null)
244                    .commit();
245        }
246    }
247
248    // TODO if we add meta-mailboxes to the database, remove this class entirely
249    private static final class MailboxPickerLoader extends CursorLoader {
250        private final long mAccountId;
251        private final boolean mAllowUnread;
252        public MailboxPickerLoader(Context context, Uri uri, String[] projection, String selection,
253                String[] selectionArgs, String sortOrder, long accountId, boolean allowUnread) {
254            super(context, uri, projection, selection, selectionArgs, sortOrder);
255            mAccountId = accountId;
256            mAllowUnread = allowUnread;
257        }
258
259        @Override
260        public Cursor loadInBackground() {
261            MatrixCursor unreadCursor =
262                    new MatrixCursor(MailboxShortcutPickerFragment.MATRIX_PROJECTION);
263            Context context = getContext();
264            if (mAllowUnread) {
265                // For the special mailboxes, their ID is < 0. The UI list does not deal with
266                // negative values very well, so, add MAX_VALUE to ensure they're positive, but,
267                // don't clash with legitimate mailboxes.
268                String mailboxName = context.getString(R.string.picker_mailbox_name_all_unread);
269                unreadCursor.addRow(
270                        new Object[] {
271                            Integer.MAX_VALUE + Mailbox.QUERY_ALL_UNREAD,
272                            Mailbox.QUERY_ALL_UNREAD,
273                            mailboxName,
274                        });
275            }
276
277            if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
278                // Do something special for the "combined" view
279                MatrixCursor combinedMailboxesCursor =
280                        new MatrixCursor(MailboxShortcutPickerFragment.MATRIX_PROJECTION);
281                // For the special mailboxes, their ID is < 0. The UI list does not deal with
282                // negative values very well, so, add MAX_VALUE to ensure they're positive, but,
283                // don't clash with legitimate mailboxes.
284                String mailboxName = context.getString(R.string.picker_mailbox_name_all_inbox);
285                combinedMailboxesCursor.addRow(
286                        new Object[] {
287                            Integer.MAX_VALUE + Mailbox.QUERY_ALL_INBOXES,
288                            Mailbox.QUERY_ALL_INBOXES,
289                            mailboxName
290                        });
291                return new MergeCursor(new Cursor[] { combinedMailboxesCursor, unreadCursor });
292            }
293
294            // Loading for a regular account; perform a normal load
295            return new MergeCursor(new Cursor[] { super.loadInBackground(), unreadCursor });
296        }
297    }
298
299    /** Mailbox picker */
300    public static class MailboxShortcutPickerFragment extends ShortcutPickerFragment {
301        static final String ARG_ACCOUNT = "MailboxShortcutPickerFragment.account";
302        private final static String REAL_ID = "realId";
303        private final static String[] MAILBOX_FROM_COLUMNS = new String[] {
304            MailboxColumns.DISPLAY_NAME,
305        };
306        /** Loader projection used for IMAP & POP3 accounts */
307        private final static String[] IMAP_PROJECTION = new String [] {
308            MailboxColumns.ID, MailboxColumns.ID + " as " + REAL_ID,
309            MailboxColumns.SERVER_ID + " as " + MailboxColumns.DISPLAY_NAME
310        };
311        /** Loader projection used for EAS accounts */
312        private final static String[] EAS_PROJECTION = new String [] {
313            MailboxColumns.ID, MailboxColumns.ID + " as " + REAL_ID,
314            MailboxColumns.DISPLAY_NAME
315        };
316        /** Loader projection used for a matrix cursor */
317        private final static String[] MATRIX_PROJECTION = new String [] {
318            MailboxColumns.ID, REAL_ID, MailboxColumns.DISPLAY_NAME
319        };
320        // TODO #ALL_MAILBOX_SELECTION is identical to MailboxesAdapter#ALL_MAILBOX_SELECTION;
321        // create a common selection. Move this to the Mailbox class?
322        /** Selection for all visible mailboxes for an account */
323        private final static String ALL_MAILBOX_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?" +
324                " AND " + Mailbox.USER_VISIBLE_MAILBOX_SELECTION;
325        /** Selection for just the INBOX of an account */
326        private final static String INBOX_ONLY_SELECTION = ALL_MAILBOX_SELECTION +
327                    " AND " + MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX;
328        /** The currently selected account */
329        private Account mAccount;
330
331        @Override
332        public void onAttach(Activity activity) {
333            // Need to setup the account first thing
334            mAccount = getArguments().getParcelable(ARG_ACCOUNT);
335            super.onAttach(activity);
336        }
337
338        @Override
339        public void onActivityCreated(Bundle savedInstanceState) {
340            super.onActivityCreated(savedInstanceState);
341            getActivity().setTitle(R.string.mailbox_shortcut_picker_title);
342        }
343
344        @Override
345        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
346            Cursor cursor = (Cursor) parent.getItemAtPosition(position);
347            long mailboxId = cursor.getLong(cursor.getColumnIndex(REAL_ID));
348            mCallback.onSelected(mAccount, mailboxId);
349        }
350
351        @Override
352        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
353            Context context = getActivity();
354            // TODO Create a fully-qualified path name for Exchange accounts [code should also work
355            //      for MoveMessageToDialog.java]
356            HostAuth recvAuth = mAccount.getOrCreateHostAuthRecv(context);
357            final String[] projection;
358            final String orderBy;
359            final String selection;
360            if (recvAuth.isEasConnection()) {
361                projection = EAS_PROJECTION;
362                orderBy = MailboxColumns.DISPLAY_NAME;
363            } else {
364                projection = IMAP_PROJECTION;
365                orderBy = MailboxColumns.SERVER_ID;
366            }
367            if ((getFilter() & FILTER_INBOX_ONLY) == 0) {
368                selection = ALL_MAILBOX_SELECTION;
369            } else {
370                selection = INBOX_ONLY_SELECTION;
371            }
372            return new MailboxPickerLoader(
373                context, Mailbox.CONTENT_URI, projection, selection,
374                new String[] { Long.toString(mAccount.mId) }, orderBy, mAccount.mId,
375                (getFilter() & FILTER_ALLOW_UNREAD) != 0);
376        }
377
378        @Override
379        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
380            // No accounts; close the dialog
381            if (data.getCount() == 0) {
382                mCallback.onMissingData(false, true);
383                return;
384            }
385            // if there is only one mailbox, auto-select it
386            if (data.getCount() == 1 && data.moveToFirst()) {
387                long mailboxId = data.getLong(data.getColumnIndex(REAL_ID));
388                mCallback.onSelected(mAccount, mailboxId);
389                return;
390            }
391            super.onLoadFinished(loader, data);
392        }
393
394        @Override
395        String[] getFromColumns() {
396            return MAILBOX_FROM_COLUMNS;
397        }
398    }
399}
400