EmailActivity.java revision ab40c988216b32ed145c0cad45c25e9cf2509c85
1/* 2 * Copyright (C) 2010 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.Controller; 20import com.android.email.ControllerResultUiThreadWrapper; 21import com.android.email.Email; 22import com.android.email.MessagingExceptionStrings; 23import com.android.email.R; 24import com.android.emailcommon.Logging; 25import com.android.emailcommon.mail.MessagingException; 26import com.android.emailcommon.provider.EmailContent.Account; 27import com.android.emailcommon.provider.EmailContent.MailboxColumns; 28import com.android.emailcommon.provider.EmailContent.Message; 29import com.android.emailcommon.provider.Mailbox; 30import com.android.emailcommon.service.SearchParams; 31import com.android.emailcommon.utility.EmailAsyncTask; 32 33import android.app.Activity; 34import android.app.AlertDialog; 35import android.app.Dialog; 36import android.app.Fragment; 37import android.app.SearchManager; 38import android.content.ContentResolver; 39import android.content.ContentUris; 40import android.content.ContentValues; 41import android.content.Context; 42import android.content.DialogInterface; 43import android.content.Intent; 44import android.os.Bundle; 45import android.os.Handler; 46import android.text.TextUtils; 47import android.util.Log; 48import android.view.Menu; 49import android.view.MenuItem; 50import android.view.View; 51import android.widget.TextView; 52 53import java.util.ArrayList; 54 55/** 56 * The main Email activity, which is used on both the tablet and the phone. 57 * 58 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by 59 * the UIController. 60 */ 61public class EmailActivity extends Activity implements View.OnClickListener, FragmentInstallable { 62 private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; 63 private static final String EXTRA_MAILBOX_ID = "MAILBOX_ID"; 64 private static final String EXTRA_MESSAGE_ID = "MESSAGE_ID"; 65 private static final String EXTRA_FORCE_PANE_MODE = "FORCE_PANE_MODE"; 66 67 /** Loader IDs starting with this is safe to use from UIControllers. */ 68 static final int UI_CONTROLLER_LOADER_ID_BASE = 100; 69 70 /** Loader IDs starting with this is safe to use from ActionBarController. */ 71 static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200; 72 73 private static final int MAILBOX_SYNC_FREQUENCY_DIALOG = 1; 74 private static final int MAILBOX_SYNC_LOOKBACK_DIALOG = 2; 75 76 private Context mContext; 77 private Controller mController; 78 private Controller.Result mControllerResult; 79 80 private UIControllerBase mUIController; 81 82 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 83 84 /** Banner to display errors */ 85 private BannerController mErrorBanner; 86 /** Id of the account that had a messaging exception most recently. */ 87 private long mLastErrorAccountId; 88 89 // STOPSHIP Temporary mailbox settings UI 90 private int mDialogSelection = -1; 91 92 /** 93 * Create an intent to launch and open account's inbox. 94 * 95 * @param accountId If -1, default account will be used. 96 */ 97 public static Intent createOpenAccountIntent(Activity fromActivity, long accountId) { 98 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 99 if (accountId != -1) { 100 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 101 } 102 return i; 103 } 104 105 /** 106 * Create an intent to launch and open a mailbox. 107 * 108 * @param accountId must not be -1. 109 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 110 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 111 */ 112 public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId, 113 long mailboxId) { 114 if (accountId == -1 || mailboxId == -1) { 115 throw new IllegalArgumentException(); 116 } 117 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 118 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 119 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 120 return i; 121 } 122 123 /** 124 * Create an intent to launch and open a message. 125 * 126 * @param accountId must not be -1. 127 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 128 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 129 * @param messageId must not be -1. 130 */ 131 public static Intent createOpenMessageIntent(Activity fromActivity, long accountId, 132 long mailboxId, long messageId) { 133 if (accountId == -1 || mailboxId == -1 || messageId == -1) { 134 throw new IllegalArgumentException(); 135 } 136 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 137 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 138 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 139 i.putExtra(EXTRA_MESSAGE_ID, messageId); 140 return i; 141 } 142 143 /** 144 * Set a debug flag to an intent to force open in 1-pane or 2-pane. 145 * 146 * @param useTwoPane true to open in 2-pane. false to open in 1-pane. 147 */ 148 public static void forcePaneMode(Intent i, boolean useTwoPane) { 149 i.putExtra(EXTRA_FORCE_PANE_MODE, useTwoPane ? 2 : 1); 150 } 151 152 /** 153 * Initialize {@link #mUIController}. 154 */ 155 private void initUIController() { 156 final boolean twoPane; 157 switch (getIntent().getIntExtra(EXTRA_FORCE_PANE_MODE, -1)) { 158 case 1: 159 twoPane = false; 160 break; 161 case 2: 162 twoPane = true; 163 break; 164 default: 165 twoPane = getResources().getBoolean(R.bool.use_two_pane); 166 break; 167 } 168 mUIController = twoPane ? new UIControllerTwoPane(this) : new UIControllerOnePane(this); 169 } 170 171 @Override 172 protected void onCreate(Bundle savedInstanceState) { 173 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onCreate"); 174 175 // UIController is used in onPrepareOptionsMenu(), which can be called from within 176 // super.onCreate(), so we need to initialize it here. 177 initUIController(); 178 179 super.onCreate(savedInstanceState); 180 ActivityHelper.debugSetWindowFlags(this); 181 setContentView(mUIController.getLayoutId()); 182 183 mUIController.onActivityViewReady(); 184 185 mContext = getApplicationContext(); 186 mController = Controller.getInstance(this); 187 mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(), 188 new ControllerResult()); 189 mController.addResultCallback(mControllerResult); 190 191 // Set up views 192 // TODO Probably better to extract mErrorMessageView related code into a separate class, 193 // so that it'll be easy to reuse for the phone activities. 194 TextView errorMessage = (TextView) findViewById(R.id.error_message); 195 errorMessage.setOnClickListener(this); 196 int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height); 197 mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight); 198 199 if (savedInstanceState != null) { 200 mUIController.restoreInstanceState(savedInstanceState); 201 } else { 202 // This needs to be done after installRestoredFragments. 203 // See UIControllerTwoPane.preFragmentTransactionCheck() 204 initFromIntent(); 205 } 206 mUIController.onActivityCreated(); 207 } 208 209 private void initFromIntent() { 210 final Intent i = getIntent(); 211 final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); 212 final long mailboxId = i.getLongExtra(EXTRA_MAILBOX_ID, -1); 213 final long messageId = i.getLongExtra(EXTRA_MESSAGE_ID, -1); 214 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 215 Log.d(Logging.LOG_TAG, String.format("initFromIntent: %d %d", accountId, mailboxId)); 216 } 217 218 if (accountId != -1) { 219 mUIController.open(accountId, mailboxId, messageId); 220 } 221 } 222 223 @Override 224 protected void onSaveInstanceState(Bundle outState) { 225 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 226 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 227 } 228 super.onSaveInstanceState(outState); 229 mUIController.onSaveInstanceState(outState); 230 } 231 232 // FragmentInstallable 233 @Override 234 public void onInstallFragment(Fragment fragment) { 235 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 236 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 237 } 238 mUIController.onInstallFragment(fragment); 239 } 240 241 // FragmentInstallable 242 @Override 243 public void onUninstallFragment(Fragment fragment) { 244 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 245 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 246 } 247 mUIController.onUninstallFragment(fragment); 248 } 249 250 @Override 251 protected void onStart() { 252 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart"); 253 super.onStart(); 254 mUIController.onActivityStart(); 255 256 // STOPSHIP Temporary search UI 257 Intent intent = getIntent(); 258 if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 259 // TODO Very temporary (e.g. no database access in UI thread) 260 Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA); 261 if (appData == null) return; // ?? 262 final long accountId = appData.getLong(EXTRA_ACCOUNT_ID); 263 final long mailboxId = appData.getLong(EXTRA_MAILBOX_ID); 264 final String queryString = intent.getStringExtra(SearchManager.QUERY); 265 Log.d(Logging.LOG_TAG, queryString); 266 // Switch to search mailbox 267 // TODO How to handle search from within the search mailbox?? 268 final Controller controller = Controller.getInstance(mContext); 269 final Mailbox searchMailbox = controller.getSearchMailbox(accountId); 270 if (searchMailbox == null) return; 271 272 // Delete contents, add a placeholder 273 ContentResolver resolver = mContext.getContentResolver(); 274 resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailbox.mId, 275 null); 276 ContentValues cv = new ContentValues(); 277 cv.put(Mailbox.DISPLAY_NAME, queryString); 278 resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailbox.mId), cv, 279 null, null); 280 Message msg = new Message(); 281 msg.mMailboxKey = searchMailbox.mId; 282 msg.mAccountKey = accountId; 283 msg.mDisplayName = "Searching for " + queryString; 284 msg.mTimeStamp = Long.MAX_VALUE; // Sort on top 285 msg.save(mContext); 286 287 startActivity(createOpenMessageIntent(EmailActivity.this, 288 accountId, searchMailbox.mId, msg.mId)); 289 EmailAsyncTask.runAsyncParallel(new Runnable() { 290 @Override 291 public void run() { 292 SearchParams searchSpec = new SearchParams(SearchParams.ALL_MAILBOXES, 293 queryString); 294 controller.searchMessages(accountId, searchSpec, searchMailbox.mId); 295 }}); 296 return; 297 } 298 } 299 300 @Override 301 protected void onResume() { 302 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume"); 303 super.onResume(); 304 mUIController.onActivityResume(); 305 /** 306 * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account 307 * has been added/removed. We don't need to do that here, because we fetch the most 308 * up-to-date account list. Additionally, we detect and do the right thing if all 309 * of the accounts have been removed. 310 */ 311 } 312 313 @Override 314 protected void onPause() { 315 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause"); 316 super.onPause(); 317 mUIController.onActivityPause(); 318 } 319 320 @Override 321 protected void onStop() { 322 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop"); 323 super.onStop(); 324 mUIController.onActivityStop(); 325 } 326 327 @Override 328 protected void onDestroy() { 329 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy"); 330 mController.removeResultCallback(mControllerResult); 331 mTaskTracker.cancellAllInterrupt(); 332 mUIController.onActivityDestroy(); 333 super.onDestroy(); 334 } 335 336 @Override 337 public void onBackPressed() { 338 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 339 Log.d(Logging.LOG_TAG, this + " onBackPressed"); 340 } 341 if (!mUIController.onBackPressed(true)) { 342 // Not handled by UIController -- perform the default. i.e. close the app. 343 super.onBackPressed(); 344 } 345 } 346 347 @Override 348 public void onClick(View v) { 349 switch (v.getId()) { 350 case R.id.error_message: 351 dismissErrorMessage(); 352 break; 353 } 354 } 355 356 /** 357 * Force dismiss the error banner. 358 */ 359 private void dismissErrorMessage() { 360 mErrorBanner.dismiss(); 361 } 362 363 @Override 364 public boolean onCreateOptionsMenu(Menu menu) { 365 return mUIController.onCreateOptionsMenu(getMenuInflater(), menu); 366 } 367 368 @Override 369 public boolean onPrepareOptionsMenu(Menu menu) { 370 // STOPSHIP Temporary search/sync options UI 371 // Only show search/sync options for EAS 12.0 and later 372 boolean isEas = false; 373 boolean canSearch = false; 374 long accountId = mUIController.getActualAccountId(); 375 if (accountId > 0) { 376 // Move database operations out of the UI thread 377 if ("eas".equals(Account.getProtocol(mContext, accountId))) { 378 isEas = true; 379 Account account = Account.restoreAccountWithId(mContext, accountId); 380 if (account != null) { 381 // We should set a flag in the account indicating ability to handle search 382 String protocolVersion = account.mProtocolVersion; 383 if (Double.parseDouble(protocolVersion) >= 12.0) { 384 canSearch = true; 385 } 386 } 387 } 388 } 389 // Should use an isSearchable call to prevent search on inappropriate accounts/boxes 390 menu.findItem(R.id.search).setVisible(canSearch); 391 // Should use an isSyncable call to prevent drafts/outbox from allowing this 392 menu.findItem(R.id.sync_lookback).setVisible(isEas); 393 menu.findItem(R.id.sync_frequency).setVisible(isEas); 394 395 return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu); 396 } 397 398 @Override 399 public boolean onSearchRequested() { 400 Bundle bundle = new Bundle(); 401 bundle.putLong(EXTRA_ACCOUNT_ID, mUIController.getActualAccountId()); 402 bundle.putLong(EXTRA_MAILBOX_ID, mUIController.getSearchMailboxId()); 403 startSearch(null, false, bundle, false); 404 return true; 405 } 406 407 // STOPSHIP Set column from user options 408 private void setMailboxColumn(long mailboxId, String column, String value) { 409 if (mailboxId > 0) { 410 ContentValues cv = new ContentValues(); 411 cv.put(column, value); 412 getContentResolver().update( 413 ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId), 414 cv, null, null); 415 mUIController.onRefresh(); 416 } 417 } 418 // STOPSHIP Temporary mailbox settings UI. If this ends up being useful, it should 419 // be moved to Utility (emailcommon) 420 private int findInStringArray(String[] array, String item) { 421 int i = 0; 422 for (String str: array) { 423 if (str.equals(item)) { 424 return i; 425 } 426 i++; 427 } 428 return -1; 429 } 430 431 // STOPSHIP Temporary mailbox settings UI 432 private final DialogInterface.OnClickListener mSelectionListener = 433 new DialogInterface.OnClickListener() { 434 public void onClick(DialogInterface dialog, int which) { 435 mDialogSelection = which; 436 } 437 }; 438 439 // STOPSHIP Temporary mailbox settings UI 440 private final DialogInterface.OnClickListener mCancelListener = 441 new DialogInterface.OnClickListener() { 442 public void onClick(DialogInterface dialog, int which) { 443 } 444 }; 445 446 // STOPSHIP Temporary mailbox settings UI 447 @Override 448 @Deprecated 449 protected Dialog onCreateDialog(int id, Bundle args) { 450 final long mailboxId = mUIController.getMailboxSettingsMailboxId(); 451 if (mailboxId < 0) { 452 return null; 453 } 454 final Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId); 455 if (mailbox == null) return null; 456 switch (id) { 457 case MAILBOX_SYNC_FREQUENCY_DIALOG: 458 String freq = Integer.toString(mailbox.mSyncInterval); 459 final String[] freqValues = getResources().getStringArray( 460 R.array.account_settings_check_frequency_values_push); 461 int selection = findInStringArray(freqValues, freq); 462 // If not found, this is a push mailbox; trust me on this 463 if (selection == -1) selection = 0; 464 return new AlertDialog.Builder(this) 465 .setIconAttribute(android.R.attr.dialogIcon) 466 .setTitle(R.string.mailbox_options_check_frequency_label) 467 .setSingleChoiceItems(R.array.account_settings_check_frequency_entries_push, 468 selection, 469 mSelectionListener) 470 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 471 public void onClick(DialogInterface dialog, int which) { 472 setMailboxColumn(mailboxId, MailboxColumns.SYNC_INTERVAL, 473 freqValues[mDialogSelection]); 474 }}) 475 .setNegativeButton(R.string.cancel_action, mCancelListener) 476 .create(); 477 478 case MAILBOX_SYNC_LOOKBACK_DIALOG: 479 freq = Integer.toString(mailbox.mSyncLookback); 480 final String[] windowValues = getResources().getStringArray( 481 R.array.account_settings_mail_window_values); 482 selection = findInStringArray(windowValues, freq); 483 return new AlertDialog.Builder(this) 484 .setIconAttribute(android.R.attr.dialogIcon) 485 .setTitle(R.string.mailbox_options_lookback_label) 486 .setSingleChoiceItems(R.array.account_settings_mail_window_entries, 487 selection, 488 mSelectionListener) 489 .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { 490 public void onClick(DialogInterface dialog, int which) { 491 setMailboxColumn(mailboxId, MailboxColumns.SYNC_LOOKBACK, 492 windowValues[mDialogSelection]); 493 }}) 494 .setNegativeButton(R.string.cancel_action, mCancelListener) 495 .create(); 496 } 497 return null; 498 } 499 500 @Override 501 @SuppressWarnings("deprecation") 502 public boolean onOptionsItemSelected(MenuItem item) { 503 if (mUIController.onOptionsItemSelected(item)) { 504 return true; 505 } 506 switch (item.getItemId()) { 507 // STOPSHIP Temporary mailbox settings UI 508 case R.id.sync_lookback: 509 showDialog(MAILBOX_SYNC_LOOKBACK_DIALOG); 510 return true; 511 // STOPSHIP Temporary mailbox settings UI 512 case R.id.sync_frequency: 513 showDialog(MAILBOX_SYNC_FREQUENCY_DIALOG); 514 return true; 515 case R.id.search: 516 onSearchRequested(); 517 return true; 518 } 519 return super.onOptionsItemSelected(item); 520 } 521 522 523 /** 524 * A {@link Controller.Result} to detect connection status. 525 */ 526 private class ControllerResult extends Controller.Result { 527 @Override 528 public void sendMailCallback( 529 MessagingException result, long accountId, long messageId, int progress) { 530 handleError(result, accountId, progress); 531 } 532 533 @Override 534 public void serviceCheckMailCallback( 535 MessagingException result, long accountId, long mailboxId, int progress, long tag) { 536 handleError(result, accountId, progress); 537 } 538 539 @Override 540 public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, 541 int progress, int numNewMessages, ArrayList<Long> addedMessages) { 542 handleError(result, accountId, progress); 543 } 544 545 @Override 546 public void updateMailboxListCallback( 547 MessagingException result, long accountId, int progress) { 548 handleError(result, accountId, progress); 549 } 550 551 @Override 552 public void loadAttachmentCallback(MessagingException result, long accountId, 553 long messageId, long attachmentId, int progress) { 554 handleError(result, accountId, progress); 555 } 556 557 @Override 558 public void loadMessageForViewCallback(MessagingException result, long accountId, 559 long messageId, int progress) { 560 handleError(result, accountId, progress); 561 } 562 563 private void handleError(final MessagingException result, final long accountId, 564 int progress) { 565 if (accountId == -1) { 566 return; 567 } 568 if (result == null) { 569 if (progress > 0) { 570 // Connection now working; clear the error message banner 571 if (mLastErrorAccountId == accountId) { 572 dismissErrorMessage(); 573 } 574 } 575 } else { 576 // Connection error; show the error message banner 577 new EmailAsyncTask<Void, Void, String>(mTaskTracker) { 578 @Override 579 protected String doInBackground(Void... params) { 580 Account account = 581 Account.restoreAccountWithId(EmailActivity.this, accountId); 582 return (account == null) ? null : account.mDisplayName; 583 } 584 585 @Override 586 protected void onPostExecute(String accountName) { 587 String message = 588 MessagingExceptionStrings.getErrorString(EmailActivity.this, result); 589 if (!TextUtils.isEmpty(accountName)) { 590 // TODO Use properly designed layout. Don't just concatenate strings; 591 // which is generally poor for I18N. 592 message = message + " (" + accountName + ")"; 593 } 594 if (mErrorBanner.show(message)) { 595 mLastErrorAccountId = accountId; 596 } 597 } 598 }.executeParallel(); 599 } 600 } 601 } 602} 603