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