1 /******************************************************************************* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 *******************************************************************************/ 17 18 package com.android.mail.ui; 19 20 import android.app.Dialog; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.res.Configuration; 25 import android.nfc.NdefMessage; 26 import android.nfc.NdefRecord; 27 import android.nfc.NfcAdapter; 28 import android.nfc.NfcEvent; 29 import android.os.Bundle; 30 import android.support.v7.app.ActionBar; 31 import android.support.v7.view.ActionMode; 32 import android.support.v7.widget.Toolbar; 33 import android.view.KeyEvent; 34 import android.view.Menu; 35 import android.view.MenuItem; 36 import android.view.MotionEvent; 37 import android.view.accessibility.AccessibilityManager; 38 39 import com.android.bitmap.BitmapCache; 40 import com.android.bitmap.UnrefedBitmapCache; 41 import com.android.mail.R; 42 import com.android.mail.analytics.Analytics; 43 import com.android.mail.analytics.AnalyticsTimer; 44 import com.android.mail.bitmap.ContactResolver; 45 import com.android.mail.compose.ComposeActivity; 46 import com.android.mail.providers.Account; 47 import com.android.mail.providers.Folder; 48 import com.android.mail.providers.SearchRecentSuggestionsProvider; 49 import com.android.mail.providers.SuggestionsProvider; 50 import com.android.mail.utils.StorageLowState; 51 import com.android.mail.utils.Utils; 52 import com.android.mail.utils.ViewUtils; 53 54 import java.io.UnsupportedEncodingException; 55 import java.net.URLEncoder; 56 57 /** 58 * This is the root activity container that holds the left navigation fragment 59 * (usually a list of folders), and the main content fragment (either a 60 * conversation list or a conversation view). 61 */ 62 public class MailActivity extends AbstractMailActivity implements ControllableActivity { 63 64 /** 339KB cache fits 10 bitmaps at 33856 bytes each. */ 65 private static final int SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES = 1024 * 339; 66 private static final float SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f; 67 /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */ 68 private static final int SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100; 69 70 /** 71 * The activity controller to which we delegate most Activity lifecycle events. 72 */ 73 private ActivityController mController; 74 75 private ViewMode mViewMode; 76 77 private ToastBarOperation mPendingToastOp; 78 private boolean mAccessibilityEnabled; 79 private AccessibilityManager mAccessibilityManager; 80 81 protected ConversationListHelper mConversationListHelper; 82 83 /** 84 * The account name currently in use. Used to construct the NFC mailto: message. This needs 85 * to be static since the {@link ComposeActivity} needs to statically change the account name 86 * and have the NFC message changed accordingly. 87 */ 88 protected static String sAccountName = null; 89 90 private BitmapCache mSendersImageCache; 91 92 private CustomViewToolbar mCustomViewToolbar; 93 94 /** 95 * Create an NFC message (in the NDEF: Nfc Data Exchange Format) to instruct the recepient to 96 * send an email to the current account. 97 */ 98 private static class NdefMessageMaker implements NfcAdapter.CreateNdefMessageCallback { 99 @Override 100 public NdefMessage createNdefMessage(NfcEvent event) { 101 if (sAccountName == null) { 102 return null; 103 } 104 return getMailtoNdef(sAccountName); 105 } 106 107 /** 108 * Returns an NDEF message with a single mailto URI record 109 * for the given email address. 110 */ 111 private static NdefMessage getMailtoNdef(String account) { 112 byte[] accountBytes; 113 try { 114 accountBytes = URLEncoder.encode(account, "UTF-8").getBytes("UTF-8"); 115 } catch (UnsupportedEncodingException e) { 116 accountBytes = account.getBytes(); 117 } 118 byte prefix = 0x06; // mailto: 119 byte[] recordBytes = new byte[accountBytes.length + 1]; 120 recordBytes[0] = prefix; 121 System.arraycopy(accountBytes, 0, recordBytes, 1, accountBytes.length); 122 NdefRecord mailto = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_URI, 123 new byte[0], recordBytes); 124 return new NdefMessage(new NdefRecord[] { mailto }); 125 } 126 } 127 128 private final NdefMessageMaker mNdefHandler = new NdefMessageMaker(); 129 130 public MailActivity() { 131 super(); 132 mConversationListHelper = new ConversationListHelper(); 133 } 134 135 @Override 136 public boolean dispatchTouchEvent(MotionEvent ev) { 137 mController.onTouchEvent(ev); 138 return super.dispatchTouchEvent(ev); 139 } 140 141 /** 142 * Default implementation returns a null view mode. 143 */ 144 @Override 145 public ViewMode getViewMode() { 146 return mViewMode; 147 } 148 149 @Override 150 public void onActivityResult(int requestCode, int resultCode, Intent data) { 151 mController.onActivityResult(requestCode, resultCode, data); 152 } 153 154 @Override 155 public void onBackPressed() { 156 if (!mController.onBackPressed()) { 157 super.onBackPressed(); 158 } 159 } 160 161 @Override 162 public void onCreate(Bundle savedState) { 163 super.onCreate(savedState); 164 // Log the start time if this is launched from the launcher with no saved states 165 Intent i = getIntent(); 166 if (i != null && i.getCategories() != null && 167 i.getCategories().contains(Intent.CATEGORY_LAUNCHER)) { 168 AnalyticsTimer.getInstance().trackStart(AnalyticsTimer.COLD_START_LAUNCHER); 169 } 170 171 resetSenderImageCache(); 172 mViewMode = new ViewMode(); 173 final boolean tabletUi = Utils.useTabletUI(this.getResources()); 174 mController = ControllerFactory.forActivity(this, mViewMode, tabletUi); 175 176 setContentView(mController.getContentViewResource()); 177 178 final Toolbar toolbar = (Toolbar) findViewById(R.id.mail_toolbar); 179 if (toolbar instanceof CustomViewToolbar) { 180 // Tablets use CustomViewToolbar to override the default search menu item positioning. 181 mCustomViewToolbar = (CustomViewToolbar) toolbar; 182 mCustomViewToolbar.setController(this, mController, mViewMode); 183 mController.addConversationListLayoutListener(mCustomViewToolbar); 184 } 185 186 setSupportActionBar(toolbar); 187 toolbar.setNavigationOnClickListener(mController.getNavigationViewClickListener()); 188 189 final ActionBar actionBar = getSupportActionBar(); 190 if (actionBar != null) { 191 // Hide the app icon. 192 actionBar.setIcon(android.R.color.transparent); 193 actionBar.setDisplayUseLogoEnabled(false); 194 } 195 196 // Must be done after setting up action bar 197 mController.onCreate(savedState); 198 199 mAccessibilityManager = 200 (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE); 201 mAccessibilityEnabled = mAccessibilityManager.isEnabled(); 202 final NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); 203 if (nfcAdapter != null) { 204 nfcAdapter.setNdefPushMessageCallback(mNdefHandler, this); 205 } 206 207 // Detect presence of hardware keyboard and log it on Analytics 208 final int hardKeyboardHidden = getResources().getConfiguration().hardKeyboardHidden; 209 if (hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) { 210 Analytics.getInstance().sendEvent("configuration", "keyboard", "use_hardware_keyboard", 211 0); 212 } else { 213 Analytics.getInstance().sendEvent("configuration", "keyboard", 214 "do_not_use_hardware_keyboard", 0); 215 } 216 } 217 218 @Override 219 protected void onPostCreate(Bundle savedInstanceState) { 220 super.onPostCreate(savedInstanceState); 221 222 mController.onPostCreate(savedInstanceState); 223 } 224 225 @Override 226 public void onConfigurationChanged(Configuration newConfig) { 227 super.onConfigurationChanged(newConfig); 228 mController.onConfigurationChanged(newConfig); 229 } 230 231 @Override 232 protected void onRestart() { 233 super.onRestart(); 234 mController.onRestart(); 235 } 236 237 /** 238 * Constructs and sets the default NFC message. This message instructs the receiver to send 239 * email to the account provided as the argument. This message is to be shared with 240 * "zero-clicks" using NFC. The message will be available as long as the current activity is in 241 * the foreground. 242 * 243 * @param account The email address to send mail to. 244 */ 245 public static void setNfcMessage(String account) { 246 sAccountName = account; 247 } 248 249 @Override 250 protected void onRestoreInstanceState(Bundle savedInstanceState) { 251 super.onRestoreInstanceState(savedInstanceState); 252 mController.onRestoreInstanceState(savedInstanceState); 253 } 254 255 @Override 256 public Dialog onCreateDialog(int id, Bundle bundle) { 257 final Dialog dialog = mController.onCreateDialog(id, bundle); 258 return dialog == null ? super.onCreateDialog(id, bundle) : dialog; 259 } 260 261 @Override 262 public boolean onCreateOptionsMenu(Menu menu) { 263 return mController.onCreateOptionsMenu(menu) || super.onCreateOptionsMenu(menu); 264 } 265 266 @Override 267 public boolean onKeyDown(int keyCode, KeyEvent event) { 268 return mController.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); 269 } 270 271 @Override 272 public boolean onOptionsItemSelected(MenuItem item) { 273 return mController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); 274 } 275 276 @Override 277 public void onPause() { 278 super.onPause(); 279 mController.onPause(); 280 } 281 282 @Override 283 public void onPrepareDialog(int id, Dialog dialog, Bundle bundle) { 284 super.onPrepareDialog(id, dialog, bundle); 285 mController.onPrepareDialog(id, dialog, bundle); 286 } 287 288 @Override 289 public boolean onPrepareOptionsMenu(Menu menu) { 290 mController.onPrepareOptionsMenu(menu); 291 return super.onPrepareOptionsMenu(menu); 292 } 293 294 @Override 295 public void onResume() { 296 super.onResume(); 297 mController.onResume(); 298 final boolean enabled = mAccessibilityManager.isEnabled(); 299 if (enabled != mAccessibilityEnabled) { 300 onAccessibilityStateChanged(enabled); 301 } 302 // App has resumed, re-check the top-level storage situation. 303 StorageLowState.checkStorageLowMode(this); 304 } 305 306 @Override 307 public void onSaveInstanceState(Bundle outState) { 308 super.onSaveInstanceState(outState); 309 mController.onSaveInstanceState(outState); 310 } 311 312 @Override 313 protected void onStart() { 314 super.onStart(); 315 mController.onStart(); 316 } 317 318 @Override 319 public boolean onSearchRequested() { 320 mController.startSearch(); 321 return true; 322 } 323 324 @Override 325 protected void onStop() { 326 super.onStop(); 327 mController.onStop(); 328 } 329 330 @Override 331 protected void onDestroy() { 332 super.onDestroy(); 333 mController.onDestroy(); 334 335 if (mCustomViewToolbar != null) { 336 mCustomViewToolbar.onDestroy(); 337 } 338 } 339 340 @Override 341 public void onWindowFocusChanged(boolean hasFocus) { 342 super.onWindowFocusChanged(hasFocus); 343 mController.onWindowFocusChanged(hasFocus); 344 } 345 346 @Override 347 public String toString() { 348 final StringBuilder sb = new StringBuilder(super.toString()); 349 sb.append("{ViewMode="); 350 sb.append(mViewMode); 351 sb.append(" controller="); 352 sb.append(mController); 353 sb.append(" current_focus="); 354 sb.append(getCurrentFocus()); 355 sb.append("}"); 356 return sb.toString(); 357 } 358 359 @Override 360 public ConversationListCallbacks getListHandler() { 361 return mController; 362 } 363 364 @Override 365 public FolderSelector getFolderSelector() { 366 return mController; 367 } 368 369 @Override 370 public FolderController getFolderController() { 371 return mController; 372 } 373 374 @Override 375 public ConversationCheckedSet getCheckedSet() { 376 return mController.getCheckedSet(); 377 } 378 379 @Override 380 public void onUndoAvailable(ToastBarOperation undoOp) { 381 mController.onUndoAvailable(undoOp); 382 } 383 384 @Override 385 public Folder getHierarchyFolder() { 386 return mController.getHierarchyFolder(); 387 } 388 389 @Override 390 public ConversationUpdater getConversationUpdater() { 391 return mController; 392 } 393 394 @Override 395 public ErrorListener getErrorListener() { 396 return mController; 397 } 398 399 @Override 400 public void setPendingToastOperation(ToastBarOperation op) { 401 mPendingToastOp = op; 402 } 403 404 @Override 405 public ToastBarOperation getPendingToastOperation() { 406 return mPendingToastOp; 407 } 408 409 @Override 410 public void onAnimationEnd(AnimatedAdapter animatedAdapter) { 411 mController.onAnimationEnd(animatedAdapter); 412 } 413 414 @Override 415 public AccountController getAccountController() { 416 return mController; 417 } 418 419 @Override 420 public RecentFolderController getRecentFolderController() { 421 return mController; 422 } 423 424 @Override 425 public DrawerController getDrawerController() { 426 return mController.getDrawerController(); 427 } 428 429 @Override 430 public KeyboardNavigationController getKeyboardNavigationController() { 431 return mController; 432 } 433 434 @Override 435 public void onFooterViewLoadMoreClick(Folder folder) { 436 mController.onFooterViewLoadMoreClick(folder); 437 } 438 439 @Override 440 public boolean isAccessibilityEnabled() { 441 return mAccessibilityEnabled; 442 } 443 444 public void onAccessibilityStateChanged(boolean enabled) { 445 mAccessibilityEnabled = enabled; 446 mController.onAccessibilityStateChanged(); 447 } 448 449 @Override 450 public final ConversationListHelper getConversationListHelper() { 451 return mConversationListHelper; 452 } 453 454 @Override 455 public FragmentLauncher getFragmentLauncher() { 456 return mController; 457 } 458 459 @Override 460 public ContactLoaderCallbacks getContactLoaderCallbacks() { 461 return new ContactLoaderCallbacks(getActivityContext()); 462 } 463 464 @Override 465 public ContactResolver getContactResolver(ContentResolver resolver, BitmapCache bitmapCache) { 466 return new ContactResolver(resolver, bitmapCache); 467 } 468 469 @Override 470 public BitmapCache getSenderImageCache() { 471 return mSendersImageCache; 472 } 473 474 @Override 475 public void resetSenderImageCache() { 476 mSendersImageCache = createNewSenderImageCache(); 477 } 478 479 private BitmapCache createNewSenderImageCache() { 480 return new UnrefedBitmapCache(Utils.isLowRamDevice(this) ? 481 0 : SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES, 482 SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION, 483 SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY); 484 } 485 486 @Override 487 public void showHelp(Account account, int viewMode) { 488 int helpContext = ViewMode.isConversationMode(viewMode) 489 ? R.string.conversation_view_help_context 490 : R.string.conversation_list_help_context; 491 Utils.showHelp(this, account, getString(helpContext)); 492 } 493 494 public SearchRecentSuggestionsProvider getSuggestionsProvider() { 495 return new SuggestionsProvider(this); 496 } 497 498 @Override 499 public void onSupportActionModeStarted(ActionMode mode) { 500 super.onSupportActionModeStarted(mode); 501 ViewUtils.setStatusBarColor(this, R.color.action_mode_statusbar_color); 502 } 503 504 @Override 505 public void onSupportActionModeFinished(ActionMode mode) { 506 super.onSupportActionModeFinished(mode); 507 ViewUtils.setStatusBarColor(this, R.color.mail_activity_status_bar_color); 508 } 509 } 510