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 17 package com.android.email.activity; 18 19 import com.android.email.R; 20 import com.android.emailcommon.provider.Account; 21 import com.android.emailcommon.provider.EmailContent.AccountColumns; 22 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 23 import com.android.emailcommon.provider.HostAuth; 24 import com.android.emailcommon.provider.Mailbox; 25 26 import android.app.Activity; 27 import android.app.FragmentTransaction; 28 import android.app.ListFragment; 29 import android.app.LoaderManager.LoaderCallbacks; 30 import android.content.ContentValues; 31 import android.content.Context; 32 import android.content.CursorLoader; 33 import android.content.Loader; 34 import android.content.res.Resources; 35 import android.database.Cursor; 36 import android.database.MatrixCursor; 37 import android.database.MatrixCursor.RowBuilder; 38 import android.database.MergeCursor; 39 import android.net.Uri; 40 import android.os.Bundle; 41 import android.view.View; 42 import android.widget.AdapterView; 43 import android.widget.AdapterView.OnItemClickListener; 44 import android.widget.ListView; 45 import 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 */ 53 public 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