QuickContactActivity.java revision 30cfd121ad8c8adb83cf417ff1d40a8ba1e3761d
1/* 2 * Copyright (C) 2009 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.contacts.quickcontact; 18 19import android.animation.ArgbEvaluator; 20import android.animation.ObjectAnimator; 21import android.app.Activity; 22import android.app.Fragment; 23import android.app.LoaderManager.LoaderCallbacks; 24import android.content.ActivityNotFoundException; 25import android.content.ContentUris; 26import android.content.Intent; 27import android.content.Loader; 28import android.content.pm.PackageManager; 29import android.graphics.Bitmap; 30import android.graphics.Color; 31import android.graphics.drawable.BitmapDrawable; 32import android.graphics.drawable.ColorDrawable; 33import android.graphics.drawable.Drawable; 34import android.graphics.PorterDuff; 35import android.graphics.PorterDuffColorFilter; 36import android.net.Uri; 37import android.os.AsyncTask; 38import android.os.Bundle; 39import android.os.Trace; 40import android.provider.ContactsContract; 41import android.provider.ContactsContract.CommonDataKinds.Email; 42import android.provider.ContactsContract.CommonDataKinds.Phone; 43import android.provider.ContactsContract.CommonDataKinds.SipAddress; 44import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 45import android.provider.ContactsContract.CommonDataKinds.Website; 46import android.provider.ContactsContract.Contacts; 47import android.provider.ContactsContract.QuickContact; 48import android.provider.ContactsContract.RawContacts; 49import android.support.v7.graphics.Palette; 50import android.text.TextUtils; 51import android.util.Log; 52import android.view.Menu; 53import android.view.MenuItem; 54import android.view.MenuInflater; 55import android.view.View; 56import android.view.View.OnClickListener; 57import android.view.WindowManager; 58import android.widget.ImageView; 59import android.widget.Toast; 60import android.widget.Toolbar; 61 62import com.android.contacts.ContactSaveService; 63import com.android.contacts.ContactsActivity; 64import com.android.contacts.common.Collapser; 65import com.android.contacts.R; 66import com.android.contacts.common.editor.SelectAccountDialogFragment; 67import com.android.contacts.common.lettertiles.LetterTileDrawable; 68import com.android.contacts.common.list.ShortcutIntentBuilder; 69import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 70import com.android.contacts.common.model.AccountTypeManager; 71import com.android.contacts.common.model.Contact; 72import com.android.contacts.common.model.ContactLoader; 73import com.android.contacts.common.model.RawContact; 74import com.android.contacts.common.model.account.AccountType; 75import com.android.contacts.common.model.account.AccountWithDataSet; 76import com.android.contacts.common.model.dataitem.DataItem; 77import com.android.contacts.common.model.dataitem.DataKind; 78import com.android.contacts.common.model.dataitem.EmailDataItem; 79import com.android.contacts.common.model.dataitem.ImDataItem; 80import com.android.contacts.common.model.dataitem.PhoneDataItem; 81import com.android.contacts.common.util.DataStatus; 82import com.android.contacts.detail.ContactDetailDisplayUtils; 83import com.android.contacts.common.util.UriUtils; 84import com.android.contacts.interactions.CalendarInteractionsLoader; 85import com.android.contacts.interactions.ContactDeletionInteraction; 86import com.android.contacts.interactions.ContactInteraction; 87import com.android.contacts.interactions.SmsInteractionsLoader; 88import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 89import com.android.contacts.util.ImageViewDrawableSetter; 90import com.android.contacts.util.SchedulingUtils; 91import com.android.contacts.widget.MultiShrinkScroller; 92import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 93 94import com.google.common.base.Preconditions; 95import com.google.common.collect.Lists; 96 97import java.util.ArrayList; 98import java.util.Arrays; 99import java.util.Collection; 100import java.util.Collections; 101import java.util.Comparator; 102import java.util.HashMap; 103import java.util.HashSet; 104import java.util.List; 105import java.util.Map; 106import java.util.Set; 107 108/** 109 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 110 * data asynchronously, and then shows a popup with details centered around 111 * {@link Intent#getSourceBounds()}. 112 */ 113public class QuickContactActivity extends ContactsActivity { 114 115 /** 116 * QuickContacts immediately takes up the full screen. All possible information is shown. 117 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 118 * should only be used by the Contacts app. 119 */ 120 public static final int MODE_FULLY_EXPANDED = 4; 121 122 private static final String TAG = "QuickContact"; 123 124 private static final int ANIMATION_SLIDE_OPEN_DURATION = 250; 125 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 75; 126 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 127 private static final float SYSTEM_BAR_BRIGHTNESS_FACTOR = 0.7f; 128 private static final int SHIM_COLOR = Color.argb(0x7F, 0, 0, 0); 129 130 /** This is the Intent action to install a shortcut in the launcher. */ 131 private static final String ACTION_INSTALL_SHORTCUT = 132 "com.android.launcher.action.INSTALL_SHORTCUT"; 133 134 @SuppressWarnings("deprecation") 135 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 136 137 private Uri mLookupUri; 138 private String[] mExcludeMimes; 139 private int mExtraMode; 140 private int mStatusBarColor; 141 private boolean mHasAlreadyBeenOpened; 142 143 private ImageView mPhotoView; 144 private ExpandingEntryCardView mCommunicationCard; 145 private ExpandingEntryCardView mRecentCard; 146 private MultiShrinkScroller mScroller; 147 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 148 private AsyncTask<Void, Void, Void> mEntriesAndActionsTask; 149 150 private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3; 151 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 152 153 private Contact mContactData; 154 private ContactLoader mContactLoader; 155 156 private PorterDuffColorFilter mColorFilter; 157 List<Drawable> mDrawablesToTint; 158 159 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 160 161 /** 162 * Keeps the default action per mimetype. Empty if no default actions are set 163 */ 164 private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); 165 166 /** 167 * Set of {@link Action} that are associated with the aggregate currently 168 * displayed by this dialog, represented as a map from {@link String} 169 * MIME-type to a list of {@link Action}. 170 */ 171 private ActionMultiMap mActions = new ActionMultiMap(); 172 173 /** 174 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 175 * 176 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 177 * in the order specified here.</p> 178 * 179 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 180 * specified here.</p> 181 * 182 * <p>The rest go between them, in the order in the array.</p> 183 */ 184 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 185 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 186 187 /** See {@link #LEADING_MIMETYPES}. */ 188 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 189 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 190 191 /** Id for the background contact loader */ 192 private static final int LOADER_CONTACT_ID = 0; 193 194 /** Id for the background Sms Loader */ 195 private static final int LOADER_SMS_ID = 1; 196 private static final String KEY_LOADER_EXTRA_SMS_PHONES = 197 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_SMS_PHONES"; 198 private static final int MAX_SMS_RETRIEVE = 3; 199 private static final int LOADER_CALENDAR_ID = 2; 200 private static final String KEY_LOADER_EXTRA_CALENDAR_EMAILS = 201 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_CALENDAR_EMAILS"; 202 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 203 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 204 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 205 180L * 24L * 60L * 60L * 1000L /* 180 days */; 206 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 207 36L * 60L * 60L * 1000L /* 36 hours */; 208 209 private static final int[] mRecentLoaderIds = new int[]{LOADER_SMS_ID, LOADER_CALENDAR_ID}; 210 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults; 211 212 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 213 214 final OnClickListener mEntryClickHandler = new OnClickListener() { 215 @Override 216 public void onClick(View v) { 217 Log.i(TAG, "mEntryClickHandler onClick"); 218 Object intent = v.getTag(); 219 if (intent == null || !(intent instanceof Intent)) { 220 return; 221 } 222 startActivity((Intent) intent); 223 } 224 }; 225 226 /** 227 * Headless fragment used to handle account selection callbacks invoked from 228 * {@link DirectoryContactUtil}. 229 */ 230 public static class SelectAccountDialogFragmentListener extends Fragment 231 implements SelectAccountDialogFragment.Listener { 232 233 private QuickContactActivity mQuickContactActivity; 234 235 public SelectAccountDialogFragmentListener() {} 236 237 @Override 238 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 239 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 240 account, mQuickContactActivity); 241 } 242 243 @Override 244 public void onAccountSelectorCancelled() {} 245 246 /** 247 * Set the parent activity. Since rotation can cause this fragment to be used across 248 * more than one activity instance, we need to explicitly set this value instead 249 * of making this class non-static. 250 */ 251 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 252 mQuickContactActivity = quickContactActivity; 253 } 254 } 255 256 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 257 = new MultiShrinkScrollerListener() { 258 @Override 259 public void onScrolledOffBottom() { 260 onBackPressed(); 261 } 262 263 @Override 264 public void onEnterFullscreen() { 265 updateStatusBarColor(); 266 } 267 268 @Override 269 public void onExitFullscreen() { 270 updateStatusBarColor(); 271 } 272 }; 273 274 @Override 275 protected void onCreate(Bundle savedInstanceState) { 276 Trace.beginSection("onCreate()"); 277 super.onCreate(savedInstanceState); 278 279 getWindow().setStatusBarColor(Color.TRANSPARENT); 280 // Since we can't disable Window animations from the Launcher, we can minimize the 281 // silliness of the animation by setting the navigation bar transparent. 282 getWindow().setNavigationBarColor(Color.TRANSPARENT); 283 284 processIntent(getIntent()); 285 286 // Show QuickContact in front of soft input 287 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 288 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 289 290 setContentView(R.layout.quickcontact_activity); 291 292 mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 293 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 294 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 295 296 mCommunicationCard.setOnClickListener(mEntryClickHandler); 297 mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title)); 298 mCommunicationCard.setExpandButtonText( 299 getResources().getString(R.string.expanding_entry_card_view_see_all)); 300 301 mRecentCard.setOnClickListener(mEntryClickHandler); 302 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 303 304 mPhotoView = (ImageView) findViewById(R.id.photo); 305 306 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 307 setActionBar(toolbar); 308 setHeaderNameText(R.string.missing_name); 309 310 mHasAlreadyBeenOpened = savedInstanceState != null; 311 312 final ColorDrawable windowShim = new ColorDrawable(SHIM_COLOR); 313 getWindow().setBackgroundDrawable(windowShim); 314 if (!mHasAlreadyBeenOpened) { 315 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 316 ObjectAnimator.ofInt(windowShim, "alpha", 0, 0xFF).setDuration(duration).start(); 317 } 318 319 if (mScroller != null) { 320 mScroller.initialize(mMultiShrinkScrollerListener); 321 if (mHasAlreadyBeenOpened) { 322 mScroller.setVisibility(View.VISIBLE); 323 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 324 } else { 325 // mScroller needs to perform asynchronous measurements after initalize(), therefore 326 // we can't mark this as GONE. 327 mScroller.setVisibility(View.INVISIBLE); 328 } 329 } 330 331 mDrawablesToTint = new ArrayList<>(); 332 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 333 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 334 if (mSelectAccountFragmentListener == null) { 335 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 336 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 337 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 338 mSelectAccountFragmentListener.setRetainInstance(true); 339 } 340 mSelectAccountFragmentListener.setQuickContactActivity(this); 341 342 Trace.endSection(); 343 } 344 345 protected void onActivityResult(int requestCode, int resultCode, 346 Intent data) { 347 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 348 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) { 349 // The contact that we were showing has been deleted. 350 finish(); 351 } 352 } 353 354 @Override 355 protected void onNewIntent(Intent intent) { 356 super.onNewIntent(intent); 357 mHasAlreadyBeenOpened = true; 358 processIntent(intent); 359 } 360 361 private void processIntent(Intent intent) { 362 Uri lookupUri = intent.getData(); 363 364 // Check to see whether it comes from the old version. 365 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 366 final long rawContactId = ContentUris.parseId(lookupUri); 367 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 368 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 369 } 370 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, 371 QuickContact.MODE_LARGE); 372 final Uri oldLookupUri = mLookupUri; 373 374 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 375 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 376 if (oldLookupUri == null) { 377 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 378 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 379 } else if (oldLookupUri != mLookupUri) { 380 // After copying a directory contact, the contact URI changes. Therefore, 381 // we need to restart the loader and reload the new contact. 382 mContactLoader = (ContactLoader) getLoaderManager().restartLoader( 383 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 384 for (int interactionLoaderId : mRecentLoaderIds) { 385 getLoaderManager().destroyLoader(interactionLoaderId); 386 } 387 } 388 } 389 390 private void runEntranceAnimation() { 391 if (mHasAlreadyBeenOpened) { 392 return; 393 } 394 mHasAlreadyBeenOpened = true; 395 final int bottomScroll = mScroller.getScrollUntilOffBottom() - 1; 396 final ObjectAnimator scrollAnimation 397 = ObjectAnimator.ofInt(mScroller, "scroll", -bottomScroll, 398 mExtraMode != MODE_FULLY_EXPANDED ? 0 : mScroller.getScrollNeededToBeFullScreen()); 399 scrollAnimation.setDuration(ANIMATION_SLIDE_OPEN_DURATION); 400 scrollAnimation.start(); 401 } 402 403 /** Assign this string to the view if it is not empty. */ 404 private void setHeaderNameText(int resId) { 405 getActionBar().setTitle(getText(resId)); 406 } 407 408 /** Assign this string to the view if it is not empty. */ 409 private void setHeaderNameText(CharSequence value) { 410 if (!TextUtils.isEmpty(value)) { 411 getActionBar().setTitle(value); 412 } 413 } 414 415 /** 416 * Check if the given MIME-type appears in the list of excluded MIME-types 417 * that the most-recent caller requested. 418 */ 419 private boolean isMimeExcluded(String mimeType) { 420 if (mExcludeMimes == null) return false; 421 for (String excludedMime : mExcludeMimes) { 422 if (TextUtils.equals(excludedMime, mimeType)) { 423 return true; 424 } 425 } 426 return false; 427 } 428 429 /** 430 * Handle the result from the ContactLoader 431 */ 432 private void bindContactData(final Contact data) { 433 Trace.beginSection("bindContactData"); 434 mContactData = data; 435 invalidateOptionsMenu(); 436 437 mDefaultsMap.clear(); 438 439 Trace.endSection(); 440 Trace.beginSection("Set display photo & name"); 441 442 mPhotoSetter.setupContactPhoto(data, mPhotoView); 443 extractAndApplyTintFromPhotoViewAsynchronously(); 444 setHeaderNameText(data.getDisplayName()); 445 446 Trace.endSection(); 447 448 final List<String> sortedActionMimeTypes = Lists.newArrayList(); 449 // Maintain a list of phone numbers to pass into SmsInteractionsLoader 450 final Set<String> phoneNumbers = new HashSet<>(); 451 // Maintain a list of email addresses to pass into CalendarInteractionsLoader 452 final Set<String> emailAddresses = new HashSet<>(); 453 // List of Entry that makes up the ExpandingEntryCardView 454 final List<Entry> entries = Lists.newArrayList(); 455 456 mEntriesAndActionsTask = new AsyncTask<Void, Void, Void>() { 457 @Override 458 protected Void doInBackground(Void... params) { 459 computeEntriesAndActions(data, phoneNumbers, emailAddresses, 460 sortedActionMimeTypes, entries); 461 return null; 462 } 463 464 @Override 465 protected void onPostExecute(Void aVoid) { 466 super.onPostExecute(aVoid); 467 // Check that original AsyncTask parameters are still valid and the activity 468 // is still running before binding to UI. A new intent could invalidate 469 // the results, for example. 470 if (data == mContactData && !isCancelled()) { 471 bindEntriesAndActions(entries, phoneNumbers, emailAddresses, 472 sortedActionMimeTypes); 473 showActivity(); 474 } 475 } 476 }; 477 mEntriesAndActionsTask.execute(); 478 } 479 480 private void bindEntriesAndActions(List<Entry> entries, 481 Set<String> phoneNumbers, 482 Set<String> emailAddresses, 483 List<String> sortedActionMimeTypes) { 484 Trace.beginSection("start sms loader"); 485 final Bundle smsExtraBundle = new Bundle(); 486 smsExtraBundle.putStringArray(KEY_LOADER_EXTRA_SMS_PHONES, 487 phoneNumbers.toArray(new String[phoneNumbers.size()])); 488 getLoaderManager().initLoader( 489 LOADER_SMS_ID, 490 smsExtraBundle, 491 mLoaderInteractionsCallbacks); 492 Trace.endSection(); 493 494 Trace.beginSection("start calendar loader"); 495 final Bundle calendarExtraBundle = new Bundle(); 496 calendarExtraBundle.putStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS, 497 emailAddresses.toArray(new String[emailAddresses.size()])); 498 getLoaderManager().initLoader( 499 LOADER_CALENDAR_ID, 500 calendarExtraBundle, 501 mLoaderInteractionsCallbacks); 502 Trace.endSection(); 503 504 Trace.beginSection("bind communicate card"); 505 if (entries.size() > 0) { 506 mCommunicationCard.initialize(entries, 507 /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN, 508 /* isExpanded = */ false, 509 /* themeColor = */ 0); 510 } 511 512 final boolean hasData = !sortedActionMimeTypes.isEmpty(); 513 mCommunicationCard.setVisibility(hasData ? View.VISIBLE : View.GONE); 514 515 Trace.endSection(); 516 } 517 518 private void showActivity() { 519 if (mScroller != null) { 520 mScroller.setVisibility(View.VISIBLE); 521 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 522 new Runnable() { 523 @Override 524 public void run() { 525 runEntranceAnimation(); 526 } 527 }); 528 } 529 } 530 531 private void computeEntriesAndActions(Contact data, Set<String> phoneNumbers, 532 Set<String> emailAddresses, List<String> sortedActionMimeTypes, List<Entry> entries) { 533 Trace.beginSection("inflate entries and actions"); 534 535 final ResolveCache cache = ResolveCache.getInstance(this); 536 for (RawContact rawContact : data.getRawContacts()) { 537 for (DataItem dataItem : rawContact.getDataItems()) { 538 final String mimeType = dataItem.getMimeType(); 539 final AccountType accountType = rawContact.getAccountType(this); 540 final DataKind dataKind = AccountTypeManager.getInstance(this) 541 .getKindOrFallback(accountType, mimeType); 542 543 if (dataItem instanceof PhoneDataItem) { 544 phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber()); 545 } 546 547 if (dataItem instanceof EmailDataItem) { 548 emailAddresses.add(((EmailDataItem) dataItem).getAddress()); 549 } 550 551 // Skip this data item if MIME-type excluded 552 if (isMimeExcluded(mimeType)) continue; 553 554 final long dataId = dataItem.getId(); 555 final boolean isPrimary = dataItem.isPrimary(); 556 final boolean isSuperPrimary = dataItem.isSuperPrimary(); 557 558 if (dataKind != null) { 559 // Build an action for this data entry, find a mapping to a UI 560 // element, build its summary from the cursor, and collect it 561 // along with all others of this MIME-type. 562 final Action action = new DataAction(getApplicationContext(), 563 dataItem, dataKind); 564 final boolean wasAdded = considerAdd(action, cache, isSuperPrimary); 565 if (wasAdded) { 566 // Remember the default 567 if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { 568 mDefaultsMap.put(mimeType, action); 569 } 570 } 571 } 572 573 // Handle Email rows with presence data as Im entry 574 final DataStatus status = data.getStatuses().get(dataId); 575 if (status != null && dataItem instanceof EmailDataItem) { 576 final EmailDataItem email = (EmailDataItem) dataItem; 577 final ImDataItem im = ImDataItem.createFromEmail(email); 578 if (dataKind != null) { 579 final DataAction action = new DataAction(getApplicationContext(), 580 im, dataKind); 581 action.setPresence(status.getPresence()); 582 considerAdd(action, cache, isSuperPrimary); 583 } 584 } 585 } 586 } 587 588 Trace.endSection(); 589 Trace.beginSection("collapsing action list"); 590 591 // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) 592 for (List<Action> actionChildren : mActions.values()) { 593 Collapser.collapseList(actionChildren); 594 } 595 596 Trace.endSection(); 597 Trace.beginSection("sort mimetypes"); 598 599 // All the mime-types to add. 600 final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); 601 // First, add LEADING_MIMETYPES, which are most common. 602 for (String mimeType : LEADING_MIMETYPES) { 603 if (containedTypes.contains(mimeType)) { 604 sortedActionMimeTypes.add(mimeType); 605 containedTypes.remove(mimeType); 606 entries.addAll(actionsToEntries(mActions.get(mimeType))); 607 } 608 } 609 610 // Add all the remaining ones that are not TRAILING 611 for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) { 612 if (!TRAILING_MIMETYPES.contains(mimeType)) { 613 sortedActionMimeTypes.add(mimeType); 614 containedTypes.remove(mimeType); 615 entries.addAll(actionsToEntries(mActions.get(mimeType))); 616 } 617 } 618 619 // Then, add TRAILING_MIMETYPES, which are least common. 620 for (String mimeType : TRAILING_MIMETYPES) { 621 if (containedTypes.contains(mimeType)) { 622 containedTypes.remove(mimeType); 623 sortedActionMimeTypes.add(mimeType); 624 entries.addAll(actionsToEntries(mActions.get(mimeType))); 625 } 626 } 627 628 Trace.endSection(); 629 } 630 631 /** 632 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 633 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 634 * on a Nexus 5. 635 */ 636 private void extractAndApplyTintFromPhotoViewAsynchronously() { 637 if (mScroller == null) { 638 return; 639 } 640 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 641 new AsyncTask<Void, Void, Integer>() { 642 @Override 643 protected Integer doInBackground(Void... params) { 644 if (imageViewDrawable instanceof BitmapDrawable) { 645 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 646 return colorFromBitmap(bitmap); 647 } 648 if (imageViewDrawable instanceof LetterTileDrawable) { 649 // LetterTileDrawable doesn't normally draw unless it is visible. Therefore, 650 // we need to directly ask it for its color via getColor(). We could directly 651 // return this color. However, in the future Palette#generate() may incorporate 652 // saturation boosting. So I want to use Palette#generate() for the sake of 653 // consistency. 654 final LetterTileDrawable tileDrawable = (LetterTileDrawable) imageViewDrawable; 655 final int PALETTE_BITMAP_SIZE = 1; 656 final Bitmap bitmap = Bitmap.createBitmap(PALETTE_BITMAP_SIZE, 657 PALETTE_BITMAP_SIZE, Bitmap.Config.ARGB_8888); 658 // If Palette can not extract a primary color, our UX person says we are better 659 // off using the LetterTileDrawable's non vibrant color than falling back 660 // to the app's default color. 661 final int color = colorFromBitmap(bitmap); 662 if (color == 0) { 663 return tileDrawable.getColor(); 664 } else { 665 return color; 666 } 667 } 668 return 0; 669 } 670 671 @Override 672 protected void onPostExecute(Integer color) { 673 super.onPostExecute(color); 674 mColorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP); 675 // Make sure the color is valid. Also check that the Photo has not changed. If it 676 // has changed, the new tint color needs to be extracted 677 if (color != 0 && imageViewDrawable == mPhotoView.getDrawable()) { 678 // TODO: animate from the previous tint. 679 mScroller.setHeaderTintColor(color); 680 681 // Create a darker version of the actionbar color. HSV is device dependent 682 // and not perceptually-linear. Therefore, we can't say mStatusBarColor is 683 // 70% as bright as the action bar color. We can only say: it is a bit darker. 684 final float hsvComponents[] = new float[3]; 685 Color.colorToHSV(color, hsvComponents); 686 hsvComponents[2] *= SYSTEM_BAR_BRIGHTNESS_FACTOR; 687 mStatusBarColor = Color.HSVToColor(hsvComponents); 688 689 updateStatusBarColor(); 690 for (Drawable drawable : mDrawablesToTint) { 691 applyThemeColorIfAvailable(drawable); 692 } 693 mDrawablesToTint.clear(); 694 } 695 } 696 }.execute(); 697 } 698 699 private void updateStatusBarColor() { 700 if (mScroller == null) { 701 return; 702 } 703 final int desiredStatusBarColor; 704 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 705 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 706 desiredStatusBarColor = mStatusBarColor; 707 } else { 708 desiredStatusBarColor = Color.TRANSPARENT; 709 } 710 // Animate to the new color. 711 if (desiredStatusBarColor != getWindow().getStatusBarColor()) { 712 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 713 getWindow().getStatusBarColor(), desiredStatusBarColor); 714 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 715 animation.setEvaluator(new ArgbEvaluator()); 716 animation.start(); 717 } 718 } 719 720 private int colorFromBitmap(Bitmap bitmap) { 721 // Author of Palette recommends using 24 colors when analyzing profile photos. 722 final int NUMBER_OF_PALETTE_COLORS = 24; 723 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 724 if (palette != null && palette.getVibrantColor() != null) { 725 return palette.getVibrantColor().getRgb(); 726 } 727 return 0; 728 } 729 730 /** 731 * Consider adding the given {@link Action}, which will only happen if 732 * {@link PackageManager} finds an application to handle 733 * {@link Action#getIntent()}. 734 * @param action the action to handle 735 * @param resolveCache cache of applications that can handle actions 736 * @param front indicates whether to add the action to the front of the list 737 * @return true if action has been added 738 */ 739 private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) { 740 if (resolveCache.hasResolve(action)) { 741 mActions.put(action.getMimeType(), action, front); 742 return true; 743 } 744 return false; 745 } 746 747 /** 748 * Converts a list of Action into a list of Entry 749 * @param actions The list of Action to convert 750 * @return The converted list of Entry 751 */ 752 private List<Entry> actionsToEntries(List<Action> actions) { 753 List<Entry> entries = new ArrayList<>(); 754 for (Action action : actions) { 755 String header = null; 756 String body = null; 757 String footer = null; 758 Drawable icon = null; 759 switch (action.getMimeType()) { 760 case Phone.CONTENT_ITEM_TYPE: 761 header = String.valueOf(action.getBody()); 762 footer = String.valueOf(action.getSubtitle()); 763 icon = applyThemeColorIfAvailable( 764 getResources().getDrawable(R.drawable.ic_phone_24dp)); 765 break; 766 case Email.CONTENT_ITEM_TYPE: 767 header = String.valueOf(action.getBody()); 768 footer = String.valueOf(action.getSubtitle()); 769 icon = applyThemeColorIfAvailable( 770 getResources().getDrawable(R.drawable.ic_email_24dp)); 771 break; 772 case StructuredPostal.CONTENT_ITEM_TYPE: 773 header = String.valueOf(action.getBody()); 774 footer = String.valueOf(action.getSubtitle()); 775 icon = applyThemeColorIfAvailable( 776 getResources().getDrawable(R.drawable.ic_place_24dp)); 777 break; 778 default: 779 header = String.valueOf(action.getSubtitle()); 780 footer = String.valueOf(action.getBody()); 781 icon = ResolveCache.getInstance(this).getIcon(action); 782 } 783 entries.add(new Entry(icon, header, body, footer, action.getIntent(), 784 /* isEditable= */ false)); 785 786 // Add SMS in addition to phone calls 787 if (action.getMimeType().equals(Phone.CONTENT_ITEM_TYPE)) { 788 entries.add(new Entry(applyThemeColorIfAvailable(getResources().getDrawable( 789 R.drawable.ic_message_24dp)), 790 getResources().getString(R.string.send_message), null, header, 791 action.getAlternateIntent(), /* isEditable = */ false)); 792 } 793 } 794 return entries; 795 } 796 797 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 798 List<Entry> entries = new ArrayList<>(); 799 for (ContactInteraction interaction : interactions) { 800 entries.add(new Entry(applyThemeColorIfAvailable(interaction.getIcon(this)), 801 interaction.getViewHeader(this), 802 interaction.getViewBody(this), 803 interaction.getBodyIcon(this), 804 interaction.getViewFooter(this), 805 interaction.getFooterIcon(this), 806 interaction.getIntent(), 807 /* isEditable = */ false)); 808 } 809 return entries; 810 } 811 812 private LoaderCallbacks<Contact> mLoaderContactCallbacks = 813 new LoaderCallbacks<Contact>() { 814 @Override 815 public void onLoaderReset(Loader<Contact> loader) { 816 } 817 818 @Override 819 public void onLoadFinished(Loader<Contact> loader, Contact data) { 820 Trace.beginSection("onLoadFinished()"); 821 822 if (isFinishing()) { 823 return; 824 } 825 if (data.isError()) { 826 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 827 // should log the actual exception. 828 throw new IllegalStateException("Failed to load contact", data.getException()); 829 } 830 if (data.isNotFound()) { 831 if (mHasAlreadyBeenOpened) { 832 finish(); 833 } else { 834 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 835 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 836 Toast.LENGTH_LONG).show(); 837 } 838 return; 839 } 840 841 bindContactData(data); 842 843 Trace.endSection(); 844 } 845 846 @Override 847 public Loader<Contact> onCreateLoader(int id, Bundle args) { 848 if (mLookupUri == null) { 849 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 850 } 851 // Load all contact data. We need loadGroupMetaData=true to determine whether the 852 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 853 return new ContactLoader(getApplicationContext(), mLookupUri, 854 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 855 false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 856 } 857 }; 858 859 @Override 860 public void onBackPressed() { 861 if (mScroller != null) { 862 // TODO: implement exit animation if the scroller isn't already off the screen 863 finish(); 864 } else { 865 super.onBackPressed(); 866 } 867 } 868 869 @Override 870 public void finish() { 871 super.finish(); 872 873 // override transitions to skip the standard window animations 874 overridePendingTransition(0, 0); 875 } 876 877 private LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 878 new LoaderCallbacks<List<ContactInteraction>>() { 879 880 @Override 881 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 882 Log.v(TAG, "onCreateLoader"); 883 Loader<List<ContactInteraction>> loader = null; 884 switch (id) { 885 case LOADER_SMS_ID: 886 Log.v(TAG, "LOADER_SMS_ID"); 887 loader = new SmsInteractionsLoader( 888 QuickContactActivity.this, 889 args.getStringArray(KEY_LOADER_EXTRA_SMS_PHONES), 890 MAX_SMS_RETRIEVE); 891 break; 892 case LOADER_CALENDAR_ID: 893 Log.v(TAG, "LOADER_CALENDAR_ID"); 894 loader = new CalendarInteractionsLoader( 895 QuickContactActivity.this, 896 Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_CALENDAR_EMAILS)), 897 MAX_FUTURE_CALENDAR_RETRIEVE, 898 MAX_PAST_CALENDAR_RETRIEVE, 899 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 900 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 901 break; 902 } 903 return loader; 904 } 905 906 @Override 907 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 908 List<ContactInteraction> data) { 909 if (mRecentLoaderResults == null) { 910 mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>(); 911 } 912 Log.v(TAG, "onLoadFinished ~ loader.getId() " + loader.getId() + " data.size() " + 913 data.size()); 914 mRecentLoaderResults.put(loader.getId(), data); 915 916 if (isAllRecentDataLoaded()) { 917 bindRecentData(); 918 } 919 } 920 921 @Override 922 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 923 mRecentLoaderResults.remove(loader.getId()); 924 } 925 926 }; 927 928 private boolean isAllRecentDataLoaded() { 929 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 930 } 931 932 private void bindRecentData() { 933 List<ContactInteraction> allInteractions = new ArrayList<>(); 934 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 935 allInteractions.addAll(loaderInteractions); 936 } 937 938 // Sort the interactions by most recent 939 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 940 @Override 941 public int compare(ContactInteraction a, ContactInteraction b) { 942 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1; 943 } 944 }); 945 946 if (allInteractions.size() > 0) { 947 mRecentCard.initialize(contactInteractionsToEntries(allInteractions), 948 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 949 /* isExpanded = */ false, 950 /* themeColor = */ 0); 951 mRecentCard.setVisibility(View.VISIBLE); 952 } 953 } 954 955 @Override 956 protected void onStop() { 957 super.onStop(); 958 959 if (mEntriesAndActionsTask != null) { 960 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 961 // results on the UI thread. In some circumstances Activities are killed without 962 // onStop() being called. This is not a problem, because in these circumstances 963 // the entire process will be killed. 964 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 965 } 966 } 967 968 /** 969 * Applies the theme color as extracted in 970 * {@link #extractAndApplyTintFromPhotoViewAsynchronously()} if available. If the color is not 971 * available, store a reference to the drawable to tint when a color becomes available. 972 */ 973 private Drawable applyThemeColorIfAvailable(Drawable drawable) { 974 if (mColorFilter != null) { 975 drawable.setColorFilter(mColorFilter); 976 } else { 977 mDrawablesToTint.add(drawable); 978 } 979 return drawable; 980 } 981 982 /** 983 * Returns true if it is possible to edit the current contact. 984 */ 985 private boolean isContactEditable() { 986 return mContactData != null && !mContactData.isDirectoryEntry(); 987 } 988 989 private void editContact() { 990 final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); 991 mContactLoader.cacheResult(); 992 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 993 startActivityForResult(intent, REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 994 } 995 996 private void toggleStar(MenuItem starredMenuItem) { 997 // Make sure there is a contact 998 if (mLookupUri != null) { 999 // Read the current starred value from the UI instead of using the last 1000 // loaded state. This allows rapid tapping without writing the same 1001 // value several times 1002 final boolean isStarred = starredMenuItem.isChecked(); 1003 1004 // To improve responsiveness, swap out the picture (and tag) in the UI already 1005 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1006 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1007 !isStarred); 1008 1009 // Now perform the real save 1010 Intent intent = ContactSaveService.createSetStarredIntent( 1011 QuickContactActivity.this, mLookupUri, !isStarred); 1012 startService(intent); 1013 } 1014 } 1015 1016 /** 1017 * Calls into the contacts provider to get a pre-authorized version of the given URI. 1018 */ 1019 private Uri getPreAuthorizedUri(Uri uri) { 1020 final Bundle uriBundle = new Bundle(); 1021 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri); 1022 final Bundle authResponse = getContentResolver().call( 1023 ContactsContract.AUTHORITY_URI, 1024 ContactsContract.Authorization.AUTHORIZATION_METHOD, 1025 null, 1026 uriBundle); 1027 if (authResponse != null) { 1028 return (Uri) authResponse.getParcelable( 1029 ContactsContract.Authorization.KEY_AUTHORIZED_URI); 1030 } else { 1031 return uri; 1032 } 1033 } 1034 private void shareContact() { 1035 final String lookupKey = mContactData.getLookupKey(); 1036 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 1037 if (mContactData.isUserProfile()) { 1038 // User is sharing the profile. We don't want to force the receiver to have 1039 // the highly-privileged READ_PROFILE permission, so we need to request a 1040 // pre-authorized URI from the provider. 1041 shareUri = getPreAuthorizedUri(shareUri); 1042 } 1043 1044 final Intent intent = new Intent(Intent.ACTION_SEND); 1045 intent.setType(Contacts.CONTENT_VCARD_TYPE); 1046 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 1047 1048 // Launch chooser to share contact via 1049 final CharSequence chooseTitle = getText(R.string.share_via); 1050 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 1051 1052 try { 1053 this.startActivity(chooseIntent); 1054 } catch (ActivityNotFoundException ex) { 1055 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1056 } 1057 } 1058 1059 /** 1060 * Creates a launcher shortcut with the current contact. 1061 */ 1062 private void createLauncherShortcutWithContact() { 1063 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 1064 new OnShortcutIntentCreatedListener() { 1065 1066 @Override 1067 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 1068 // Broadcast the shortcutIntent to the launcher to create a 1069 // shortcut to this contact 1070 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 1071 QuickContactActivity.this.sendBroadcast(shortcutIntent); 1072 1073 // Send a toast to give feedback to the user that a shortcut to this 1074 // contact was added to the launcher. 1075 Toast.makeText(QuickContactActivity.this, 1076 R.string.createContactShortcutSuccessful, 1077 Toast.LENGTH_SHORT).show(); 1078 } 1079 1080 }); 1081 builder.createContactShortcutIntent(mLookupUri); 1082 } 1083 1084 @Override 1085 public boolean onCreateOptionsMenu(Menu menu) { 1086 MenuInflater inflater = getMenuInflater(); 1087 inflater.inflate(R.menu.quickcontact, menu); 1088 return true; 1089 } 1090 1091 @Override 1092 public boolean onPrepareOptionsMenu(Menu menu) { 1093 if (mContactData != null) { 1094 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 1095 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1096 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1097 mContactData.getStarred()); 1098 // Configure edit MenuItem 1099 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 1100 editMenuItem.setVisible(true); 1101 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 1102 .isInvisibleAndAddable(mContactData, this)) { 1103 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp); 1104 } else if (isContactEditable()) { 1105 editMenuItem.setIcon(R.drawable.ic_create_24dp); 1106 } else { 1107 editMenuItem.setVisible(false); 1108 } 1109 } 1110 return true; 1111 } 1112 1113 @Override 1114 public boolean onOptionsItemSelected(MenuItem item) { 1115 switch (item.getItemId()) { 1116 case R.id.menu_star: 1117 toggleStar(item); 1118 return true; 1119 case R.id.menu_edit: 1120 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 1121 DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(), 1122 mSelectAccountFragmentListener); 1123 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1124 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 1125 } else if (isContactEditable()) { 1126 editContact(); 1127 } 1128 return true; 1129 case R.id.menu_share: 1130 shareContact(); 1131 return true; 1132 case R.id.menu_create_contact_shortcut: 1133 createLauncherShortcutWithContact(); 1134 return true; 1135 default: 1136 return super.onOptionsItemSelected(item); 1137 } 1138 } 1139} 1140