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