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