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