EmailActivity.java revision 458816b2c8c3bc684119adba8126584c45b468e8
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.Fragment; 21import android.content.Intent; 22import android.content.res.Configuration; 23import android.os.Bundle; 24import android.os.Handler; 25import android.text.TextUtils; 26import android.util.Log; 27import android.view.Menu; 28import android.view.MenuItem; 29import android.view.View; 30import android.view.WindowManager; 31import android.widget.TextView; 32 33import com.android.email.Controller; 34import com.android.email.ControllerResultUiThreadWrapper; 35import com.android.email.Email; 36import com.android.email.MessageListContext; 37import com.android.email.MessagingExceptionStrings; 38import com.android.email.R; 39import com.android.emailcommon.Logging; 40import com.android.emailcommon.mail.MessagingException; 41import com.android.emailcommon.provider.Account; 42import com.android.emailcommon.provider.EmailContent.Message; 43import com.android.emailcommon.provider.Mailbox; 44import com.android.emailcommon.utility.EmailAsyncTask; 45import com.android.emailcommon.utility.IntentUtilities; 46import com.google.common.base.Preconditions; 47 48import java.util.ArrayList; 49 50/** 51 * The main Email activity, which is used on both the tablet and the phone. 52 * 53 * Because this activity is device agnostic, so most of the UI aren't owned by this, but by 54 * the UIController. 55 */ 56public class EmailActivity extends Activity implements View.OnClickListener, FragmentInstallable { 57 public static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; 58 public static final String EXTRA_MAILBOX_ID = "MAILBOX_ID"; 59 public static final String EXTRA_MESSAGE_ID = "MESSAGE_ID"; 60 public static final String EXTRA_FROM_KEYGUARD = "FROM_KEYGUARD"; 61 public static final String EXTRA_QUERY_STRING = "QUERY_STRING"; 62 63 /** Loader IDs starting with this is safe to use from UIControllers. */ 64 static final int UI_CONTROLLER_LOADER_ID_BASE = 100; 65 66 /** Loader IDs starting with this is safe to use from ActionBarController. */ 67 static final int ACTION_BAR_CONTROLLER_LOADER_ID_BASE = 200; 68 69 private static float sLastFontScale = -1; 70 71 private Controller mController; 72 private Controller.Result mControllerResult; 73 74 private UIControllerBase mUIController; 75 76 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 77 78 /** Banner to display errors */ 79 private BannerController mErrorBanner; 80 /** Id of the account that had a messaging exception most recently. */ 81 private long mLastErrorAccountId; 82 83 /** 84 * Create an intent to launch and open account's inbox. 85 * 86 * @param accountId If -1, default account will be used. 87 */ 88 public static Intent createOpenAccountIntent(Activity fromActivity, long accountId, 89 boolean fromKeyguard) { 90 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 91 if (accountId != -1) { 92 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 93 i.putExtra(EXTRA_FROM_KEYGUARD, fromKeyguard); 94 } 95 return i; 96 } 97 98 /** 99 * Create an intent to launch and open a mailbox. 100 * 101 * @param accountId must not be -1. 102 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 103 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 104 */ 105 public static Intent createOpenMailboxIntent(Activity fromActivity, long accountId, 106 long mailboxId, boolean fromKeyguard) { 107 if (accountId == -1 || mailboxId == -1) { 108 throw new IllegalArgumentException(); 109 } 110 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 111 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 112 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 113 i.putExtra(EXTRA_FROM_KEYGUARD, fromKeyguard); 114 return i; 115 } 116 117 /** 118 * Create an intent to launch and open a message. 119 * 120 * @param accountId must not be -1. 121 * @param mailboxId must not be -1. Magic mailboxes IDs (such as 122 * {@link Mailbox#QUERY_ALL_INBOXES}) don't work. 123 * @param messageId must not be -1. 124 */ 125 public static Intent createOpenMessageIntent(Activity fromActivity, long accountId, 126 long mailboxId, long messageId, boolean fromKeyguard) { 127 if (accountId == -1 || mailboxId == -1 || messageId == -1) { 128 throw new IllegalArgumentException(); 129 } 130 Intent i = IntentUtilities.createRestartAppIntent(fromActivity, EmailActivity.class); 131 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 132 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 133 i.putExtra(EXTRA_MESSAGE_ID, messageId); 134 i.putExtra(EXTRA_FROM_KEYGUARD, fromKeyguard); 135 return i; 136 } 137 138 /** 139 * Create an intent to launch search activity. 140 * 141 * @param accountId ID of the account for the mailbox. Must not be {@link Account#NO_ACCOUNT}. 142 * @param mailboxId ID of the mailbox to search, or {@link Mailbox#NO_MAILBOX} to perform 143 * global search. 144 * @param query query string. 145 */ 146 public static Intent createSearchIntent(Activity fromActivity, long accountId, 147 long mailboxId, String query) { 148 Preconditions.checkArgument(Account.isNormalAccount(accountId), 149 "Can only search in normal accounts"); 150 151 // Note that a search doesn't use a restart intent, as we want another instance of 152 // the activity to sit on the stack for search. 153 Intent i = new Intent(fromActivity, EmailActivity.class); 154 i.putExtra(EXTRA_ACCOUNT_ID, accountId); 155 i.putExtra(EXTRA_MAILBOX_ID, mailboxId); 156 i.putExtra(EXTRA_QUERY_STRING, query); 157 i.setAction(Intent.ACTION_SEARCH); 158 return i; 159 } 160 161 /** 162 * Initialize {@link #mUIController}. 163 */ 164 private void initUIController() { 165 if (UiUtilities.useTwoPane(this)) { 166 if (getIntent().getAction() != null 167 && Intent.ACTION_SEARCH.equals(getIntent().getAction()) 168 && !UiUtilities.showTwoPaneSearchResults(this)) { 169 mUIController = new UIControllerSearchTwoPane(this); 170 } else { 171 mUIController = new UIControllerTwoPane(this); 172 } 173 } else { 174 mUIController = new UIControllerOnePane(this); 175 } 176 } 177 178 @Override 179 protected void onCreate(Bundle savedInstanceState) { 180 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onCreate"); 181 182 float fontScale = getResources().getConfiguration().fontScale; 183 if (sLastFontScale != -1 && sLastFontScale != fontScale) { 184 // If the font scale has been initialized, and has been detected to be different than 185 // the last time the Activity ran, it means the user changed the font while no 186 // Email Activity was running - we still need to purge static information though. 187 onFontScaleChangeDetected(); 188 } 189 sLastFontScale = fontScale; 190 191 // UIController is used in onPrepareOptionsMenu(), which can be called from within 192 // super.onCreate(), so we need to initialize it here. 193 initUIController(); 194 195 super.onCreate(savedInstanceState); 196 ActivityHelper.debugSetWindowFlags(this); 197 198 final Intent intent = getIntent(); 199 boolean fromKeyguard = intent.getBooleanExtra(EXTRA_FROM_KEYGUARD, false); 200 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 201 Log.d(Logging.LOG_TAG, "FLAG_DISMISS_KEYGUARD " + fromKeyguard); 202 } 203 if (fromKeyguard) { 204 getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 205 } 206 207 setContentView(mUIController.getLayoutId()); 208 209 mUIController.onActivityViewReady(); 210 211 mController = Controller.getInstance(this); 212 mControllerResult = new ControllerResultUiThreadWrapper<ControllerResult>(new Handler(), 213 new ControllerResult()); 214 mController.addResultCallback(mControllerResult); 215 216 // Set up views 217 // TODO Probably better to extract mErrorMessageView related code into a separate class, 218 // so that it'll be easy to reuse for the phone activities. 219 TextView errorMessage = (TextView) findViewById(R.id.error_message); 220 errorMessage.setOnClickListener(this); 221 int errorBannerHeight = getResources().getDimensionPixelSize(R.dimen.error_message_height); 222 mErrorBanner = new BannerController(this, errorMessage, errorBannerHeight); 223 224 if (savedInstanceState != null) { 225 mUIController.onRestoreInstanceState(savedInstanceState); 226 } else { 227 final MessageListContext viewContext = MessageListContext.forIntent(this, intent); 228 if (viewContext == null) { 229 // This might happen if accounts were deleted on another thread, and there aren't 230 // any remaining 231 Welcome.actionStart(this); 232 finish(); 233 return; 234 } else { 235 final long messageId = intent.getLongExtra(EXTRA_MESSAGE_ID, Message.NO_MESSAGE); 236 mUIController.open(viewContext, messageId); 237 } 238 } 239 mUIController.onActivityCreated(); 240 } 241 242 @Override 243 protected void onSaveInstanceState(Bundle outState) { 244 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 245 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 246 } 247 super.onSaveInstanceState(outState); 248 mUIController.onSaveInstanceState(outState); 249 } 250 251 // FragmentInstallable 252 @Override 253 public void onInstallFragment(Fragment fragment) { 254 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 255 Log.d(Logging.LOG_TAG, this + " onInstallFragment fragment=" + fragment); 256 } 257 mUIController.onInstallFragment(fragment); 258 } 259 260 // FragmentInstallable 261 @Override 262 public void onUninstallFragment(Fragment fragment) { 263 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 264 Log.d(Logging.LOG_TAG, this + " onUninstallFragment fragment=" + fragment); 265 } 266 mUIController.onUninstallFragment(fragment); 267 } 268 269 @Override 270 protected void onStart() { 271 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStart"); 272 super.onStart(); 273 mUIController.onActivityStart(); 274 } 275 276 @Override 277 protected void onResume() { 278 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onResume"); 279 super.onResume(); 280 mUIController.onActivityResume(); 281 /** 282 * In {@link MessageList#onResume()}, we go back to {@link Welcome} if an account 283 * has been added/removed. We don't need to do that here, because we fetch the most 284 * up-to-date account list. Additionally, we detect and do the right thing if all 285 * of the accounts have been removed. 286 */ 287 } 288 289 @Override 290 protected void onPause() { 291 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onPause"); 292 super.onPause(); 293 mUIController.onActivityPause(); 294 } 295 296 @Override 297 protected void onStop() { 298 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onStop"); 299 super.onStop(); 300 mUIController.onActivityStop(); 301 } 302 303 @Override 304 protected void onDestroy() { 305 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) Log.d(Logging.LOG_TAG, this + " onDestroy"); 306 mController.removeResultCallback(mControllerResult); 307 mTaskTracker.cancellAllInterrupt(); 308 mUIController.onActivityDestroy(); 309 super.onDestroy(); 310 } 311 312 @Override 313 public void onBackPressed() { 314 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 315 Log.d(Logging.LOG_TAG, this + " onBackPressed"); 316 } 317 if (!mUIController.onBackPressed(true)) { 318 // Not handled by UIController -- perform the default. i.e. close the app. 319 super.onBackPressed(); 320 } 321 } 322 323 @Override 324 public void onClick(View v) { 325 switch (v.getId()) { 326 case R.id.error_message: 327 dismissErrorMessage(); 328 break; 329 } 330 } 331 332 @Override 333 public void onWindowFocusChanged(boolean hasFocus) { 334 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 335 Log.d(Logging.LOG_TAG, "FLAG_DISMISS_KEYGUARD onWindowFocusChanged " + hasFocus); 336 } 337 if (hasFocus) { 338 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); 339 } 340 } 341 342 /** 343 * Force dismiss the error banner. 344 */ 345 private void dismissErrorMessage() { 346 mErrorBanner.dismiss(); 347 } 348 349 @Override 350 public boolean onCreateOptionsMenu(Menu menu) { 351 return mUIController.onCreateOptionsMenu(getMenuInflater(), menu); 352 } 353 354 @Override 355 public boolean onPrepareOptionsMenu(Menu menu) { 356 return mUIController.onPrepareOptionsMenu(getMenuInflater(), menu); 357 } 358 359 /** 360 * Called when the search key is pressd. 361 * 362 * Use the below command to emulate the key press on devices without the search key. 363 * adb shell input keyevent 84 364 */ 365 @Override 366 public boolean onSearchRequested() { 367 if (Email.DEBUG) { 368 Log.d(Logging.LOG_TAG, this + " onSearchRequested"); 369 } 370 mUIController.onSearchRequested(); 371 return true; // Event handled. 372 } 373 374 @Override 375 @SuppressWarnings("deprecation") 376 public boolean onOptionsItemSelected(MenuItem item) { 377 if (mUIController.onOptionsItemSelected(item)) { 378 return true; 379 } 380 return super.onOptionsItemSelected(item); 381 } 382 383 /** 384 * A {@link Controller.Result} to detect connection status. 385 */ 386 private class ControllerResult extends Controller.Result { 387 @Override 388 public void sendMailCallback( 389 MessagingException result, long accountId, long messageId, int progress) { 390 handleError(result, accountId, progress); 391 } 392 393 @Override 394 public void serviceCheckMailCallback( 395 MessagingException result, long accountId, long mailboxId, int progress, long tag) { 396 handleError(result, accountId, progress); 397 } 398 399 @Override 400 public void updateMailboxCallback(MessagingException result, long accountId, long mailboxId, 401 int progress, int numNewMessages, ArrayList<Long> addedMessages) { 402 handleError(result, accountId, progress); 403 } 404 405 @Override 406 public void updateMailboxListCallback( 407 MessagingException result, long accountId, int progress) { 408 handleError(result, accountId, progress); 409 } 410 411 @Override 412 public void loadAttachmentCallback(MessagingException result, long accountId, 413 long messageId, long attachmentId, int progress) { 414 handleError(result, accountId, progress); 415 } 416 417 @Override 418 public void loadMessageForViewCallback(MessagingException result, long accountId, 419 long messageId, int progress) { 420 handleError(result, accountId, progress); 421 } 422 423 private void handleError(final MessagingException result, final long accountId, 424 int progress) { 425 if (accountId == -1) { 426 return; 427 } 428 if (result == null) { 429 if (progress > 0) { 430 // Connection now working; clear the error message banner 431 if (mLastErrorAccountId == accountId) { 432 dismissErrorMessage(); 433 } 434 } 435 } else { 436 Account account = Account.restoreAccountWithId(EmailActivity.this, accountId); 437 if (account == null) return; 438 String message = 439 MessagingExceptionStrings.getErrorString(EmailActivity.this, result); 440 if (!TextUtils.isEmpty(account.mDisplayName)) { 441 // TODO Use properly designed layout. Don't just concatenate strings; 442 // which is generally poor for I18N. 443 message = message + " (" + account.mDisplayName + ")"; 444 } 445 if (mErrorBanner.show(message)) { 446 mLastErrorAccountId = accountId; 447 } 448 } 449 } 450 } 451 452 /** 453 * Handle a change to the system font size. This invalidates some static caches we have. 454 */ 455 private void onFontScaleChangeDetected() { 456 MessageListItem.resetDrawingCaches(); 457 } 458} 459