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