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