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