QuickContactActivity.java revision faf973989af7ac3b735c2aee1bec22ef6608b123
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.Animator; 20import android.animation.Animator.AnimatorListener; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.ArgbEvaluator; 23import android.animation.ObjectAnimator; 24import android.app.Activity; 25import android.app.Fragment; 26import android.app.LoaderManager.LoaderCallbacks; 27import android.app.SearchManager; 28import android.content.ActivityNotFoundException; 29import android.content.ComponentName; 30import android.content.ContentUris; 31import android.content.ContentValues; 32import android.content.Intent; 33import android.content.Loader; 34import android.graphics.Bitmap; 35import android.graphics.Color; 36import android.graphics.PorterDuff; 37import android.graphics.PorterDuffColorFilter; 38import android.graphics.drawable.BitmapDrawable; 39import android.graphics.drawable.ColorDrawable; 40import android.graphics.drawable.Drawable; 41import android.net.ParseException; 42import android.net.Uri; 43import android.net.WebAddress; 44import android.os.AsyncTask; 45import android.os.Bundle; 46import android.os.Trace; 47import android.provider.CalendarContract; 48import android.provider.ContactsContract; 49import android.provider.ContactsContract.CommonDataKinds.Email; 50import android.provider.ContactsContract.CommonDataKinds.Event; 51import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 52import android.provider.ContactsContract.CommonDataKinds.Identity; 53import android.provider.ContactsContract.CommonDataKinds.Im; 54import android.provider.ContactsContract.CommonDataKinds.Nickname; 55import android.provider.ContactsContract.CommonDataKinds.Note; 56import android.provider.ContactsContract.CommonDataKinds.Organization; 57import android.provider.ContactsContract.CommonDataKinds.Phone; 58import android.provider.ContactsContract.CommonDataKinds.Relation; 59import android.provider.ContactsContract.CommonDataKinds.SipAddress; 60import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 61import android.provider.ContactsContract.CommonDataKinds.Website; 62import android.provider.ContactsContract.Contacts; 63import android.provider.ContactsContract.DisplayNameSources; 64import android.provider.ContactsContract.DataUsageFeedback; 65import android.provider.ContactsContract.QuickContact; 66import android.provider.ContactsContract.RawContacts; 67import android.support.v7.graphics.Palette; 68import android.text.TextUtils; 69import android.util.Log; 70import android.util.Pair; 71import android.view.Menu; 72import android.view.MenuInflater; 73import android.view.MenuItem; 74import android.view.View; 75import android.view.View.OnClickListener; 76import android.view.WindowManager; 77import android.widget.ImageView; 78import android.widget.Toast; 79import android.widget.Toolbar; 80 81import com.android.contacts.ContactSaveService; 82import com.android.contacts.ContactsActivity; 83import com.android.contacts.NfcHandler; 84import com.android.contacts.R; 85import com.android.contacts.common.CallUtil; 86import com.android.contacts.common.Collapser; 87import com.android.contacts.common.ContactsUtils; 88import com.android.contacts.common.editor.SelectAccountDialogFragment; 89import com.android.contacts.common.lettertiles.LetterTileDrawable; 90import com.android.contacts.common.list.ShortcutIntentBuilder; 91import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 92import com.android.contacts.common.model.AccountTypeManager; 93import com.android.contacts.common.model.Contact; 94import com.android.contacts.common.model.ContactLoader; 95import com.android.contacts.common.model.RawContact; 96import com.android.contacts.common.model.account.AccountType; 97import com.android.contacts.common.model.account.AccountWithDataSet; 98import com.android.contacts.common.model.dataitem.DataItem; 99import com.android.contacts.common.model.dataitem.DataKind; 100import com.android.contacts.common.model.dataitem.EmailDataItem; 101import com.android.contacts.common.model.dataitem.EventDataItem; 102import com.android.contacts.common.model.dataitem.ImDataItem; 103import com.android.contacts.common.model.dataitem.NicknameDataItem; 104import com.android.contacts.common.model.dataitem.NoteDataItem; 105import com.android.contacts.common.model.dataitem.OrganizationDataItem; 106import com.android.contacts.common.model.dataitem.PhoneDataItem; 107import com.android.contacts.common.model.dataitem.RelationDataItem; 108import com.android.contacts.common.model.dataitem.SipAddressDataItem; 109import com.android.contacts.common.model.dataitem.StructuredNameDataItem; 110import com.android.contacts.common.model.dataitem.StructuredPostalDataItem; 111import com.android.contacts.common.model.dataitem.WebsiteDataItem; 112import com.android.contacts.common.util.DateUtils; 113import com.android.contacts.common.util.MaterialColorMapUtils; 114import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 115import com.android.contacts.detail.ContactDetailDisplayUtils; 116import com.android.contacts.interactions.CalendarInteractionsLoader; 117import com.android.contacts.interactions.CallLogInteractionsLoader; 118import com.android.contacts.interactions.ContactDeletionInteraction; 119import com.android.contacts.interactions.ContactInteraction; 120import com.android.contacts.interactions.SmsInteractionsLoader; 121import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 122import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener; 123import com.android.contacts.util.ImageViewDrawableSetter; 124import com.android.contacts.util.PhoneCapabilityTester; 125import com.android.contacts.util.SchedulingUtils; 126import com.android.contacts.util.StructuredPostalUtils; 127import com.android.contacts.widget.MultiShrinkScroller; 128import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 129 130import com.google.common.base.Preconditions; 131import com.google.common.collect.Lists; 132 133import java.util.ArrayList; 134import java.util.Arrays; 135import java.util.Calendar; 136import java.util.Collections; 137import java.util.Comparator; 138import java.util.Date; 139import java.util.HashMap; 140import java.util.List; 141import java.util.Map; 142 143/** 144 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 145 * data asynchronously, and then shows a popup with details centered around 146 * {@link Intent#getSourceBounds()}. 147 */ 148public class QuickContactActivity extends ContactsActivity { 149 150 /** 151 * QuickContacts immediately takes up the full screen. All possible information is shown. 152 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 153 * should only be used by the Contacts app. 154 */ 155 public static final int MODE_FULLY_EXPANDED = 4; 156 157 private static final String TAG = "QuickContact"; 158 159 private static final String KEY_THEME_COLOR = "theme_color"; 160 161 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150; 162 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 163 private static final int SCRIM_COLOR = Color.argb(0xB2, 0, 0, 0); 164 private static final String SCHEME_SMSTO = "smsto"; 165 private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms"; 166 167 /** This is the Intent action to install a shortcut in the launcher. */ 168 private static final String ACTION_INSTALL_SHORTCUT = 169 "com.android.launcher.action.INSTALL_SHORTCUT"; 170 171 @SuppressWarnings("deprecation") 172 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 173 174 private Uri mLookupUri; 175 private String[] mExcludeMimes; 176 private int mExtraMode; 177 private int mStatusBarColor; 178 private boolean mHasAlreadyBeenOpened; 179 180 private ImageView mPhotoView; 181 private View mTransparentView; 182 private ExpandingEntryCardView mContactCard; 183 private ExpandingEntryCardView mRecentCard; 184 private ExpandingEntryCardView mAboutCard; 185 /** 186 * This list contains all the {@link DataItem}s. Each nested list contains all data items of a 187 * specific mimetype in sorted order, using mWithinMimeTypeDataItemComparator. The mimetype 188 * lists are sorted using mAmongstMimeTypeDataItemComparator. 189 */ 190 private List<List<DataItem>> mDataItemsList; 191 /** 192 * A map between a mimetype string and the corresponding list of data items. The data items 193 * are in sorted order using mWithinMimeTypeDataItemComparator. 194 */ 195 private Map<String, List<DataItem>> mDataItemsMap; 196 private MultiShrinkScroller mScroller; 197 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 198 private AsyncTask<Void, Void, Pair<List<List<DataItem>>, Map<String, List<DataItem>>>> 199 mEntriesAndActionsTask; 200 private ColorDrawable mWindowScrim; 201 private boolean mIsWaitingForOtherPieceOfExitAnimation; 202 private boolean mIsExitAnimationInProgress; 203 private boolean mHasComputedThemeColor; 204 private ComponentName mSmsComponent; 205 206 private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3; 207 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 208 209 private Contact mContactData; 210 private ContactLoader mContactLoader; 211 private PorterDuffColorFilter mColorFilter; 212 213 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 214 215 /** 216 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 217 * 218 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 219 * in the order specified here.</p> 220 * 221 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 222 * specified here.</p> 223 * 224 * <p>The rest go between them, in the order in the array.</p> 225 */ 226 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 227 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 228 229 /** See {@link #LEADING_MIMETYPES}. */ 230 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 231 StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 232 233 private static final List<String> ABOUT_CARD_MIMETYPES = Lists.newArrayList( 234 Event.CONTENT_ITEM_TYPE, GroupMembership.CONTENT_ITEM_TYPE, Identity.CONTENT_ITEM_TYPE, 235 Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE, 236 Organization.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 237 238 /** Id for the background contact loader */ 239 private static final int LOADER_CONTACT_ID = 0; 240 241 private static final String KEY_LOADER_EXTRA_PHONES = 242 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES"; 243 244 /** Id for the background Sms Loader */ 245 private static final int LOADER_SMS_ID = 1; 246 private static final int MAX_SMS_RETRIEVE = 3; 247 248 /** Id for the back Calendar Loader */ 249 private static final int LOADER_CALENDAR_ID = 2; 250 private static final String KEY_LOADER_EXTRA_EMAILS = 251 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS"; 252 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 253 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 254 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 255 180L * 24L * 60L * 60L * 1000L /* 180 days */; 256 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 257 36L * 60L * 60L * 1000L /* 36 hours */; 258 259 /** Id for the background Call Log Loader */ 260 private static final int LOADER_CALL_LOG_ID = 3; 261 private static final int MAX_CALL_LOG_RETRIEVE = 3; 262 263 264 private static final int[] mRecentLoaderIds = new int[]{ 265 LOADER_SMS_ID, 266 LOADER_CALENDAR_ID, 267 LOADER_CALL_LOG_ID}; 268 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults; 269 270 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 271 272 final OnClickListener mEntryClickHandler = new OnClickListener() { 273 @Override 274 public void onClick(View v) { 275 // Data Id is stored as the entry view id 276 final int dataId = v.getId(); 277 Object intentObject = v.getTag(); 278 if (intentObject == null || !(intentObject instanceof Intent)) { 279 Log.w(TAG, "Intent tag was not used correctly"); 280 return; 281 } 282 final Intent intent = (Intent) intentObject; 283 284 // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id 285 // so the exact usage type is not necessary in all cases 286 String usageType = DataUsageFeedback.USAGE_TYPE_CALL; 287 288 final String scheme = intent.getData().getScheme(); 289 if ((scheme != null && scheme.equals(SCHEME_SMSTO)) || 290 (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) { 291 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT; 292 } 293 294 // Data IDs start at 1 so anything less is invalid 295 if (dataId > 0) { 296 final Uri uri = DataUsageFeedback.FEEDBACK_URI.buildUpon() 297 .appendPath(String.valueOf(dataId)) 298 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType) 299 .build(); 300 final boolean successful = getContentResolver().update( 301 uri, new ContentValues(), null, null) > 0; 302 if (!successful) { 303 Log.w(TAG, "DataUsageFeedback increment failed"); 304 } 305 } else { 306 Log.w(TAG, "Invalid Data ID"); 307 } 308 309 startActivity(intent); 310 } 311 }; 312 313 final ExpandingEntryCardViewListener mExpandingEntryCardViewListener 314 = new ExpandingEntryCardViewListener() { 315 @Override 316 public void onCollapse(int heightDelta) { 317 mScroller.prepareForShrinkingScrollChild(heightDelta); 318 } 319 }; 320 321 /** 322 * Headless fragment used to handle account selection callbacks invoked from 323 * {@link DirectoryContactUtil}. 324 */ 325 public static class SelectAccountDialogFragmentListener extends Fragment 326 implements SelectAccountDialogFragment.Listener { 327 328 private QuickContactActivity mQuickContactActivity; 329 330 public SelectAccountDialogFragmentListener() {} 331 332 @Override 333 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 334 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 335 account, mQuickContactActivity); 336 } 337 338 @Override 339 public void onAccountSelectorCancelled() {} 340 341 /** 342 * Set the parent activity. Since rotation can cause this fragment to be used across 343 * more than one activity instance, we need to explicitly set this value instead 344 * of making this class non-static. 345 */ 346 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 347 mQuickContactActivity = quickContactActivity; 348 } 349 } 350 351 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 352 = new MultiShrinkScrollerListener() { 353 @Override 354 public void onScrolledOffBottom() { 355 if (!mIsWaitingForOtherPieceOfExitAnimation) { 356 finish(); 357 return; 358 } 359 mIsWaitingForOtherPieceOfExitAnimation = false; 360 } 361 362 @Override 363 public void onEnterFullscreen() { 364 updateStatusBarColor(); 365 } 366 367 @Override 368 public void onExitFullscreen() { 369 updateStatusBarColor(); 370 } 371 372 @Override 373 public void onStartScrollOffBottom() { 374 // Remove the window shim now that we are starting an Activity exit animation. 375 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 376 final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0xFF, 0); 377 animator.addListener(mExitWindowShimAnimationListener); 378 animator.setDuration(duration).start(); 379 mIsWaitingForOtherPieceOfExitAnimation = true; 380 mIsExitAnimationInProgress = true; 381 } 382 }; 383 384 final AnimatorListener mExitWindowShimAnimationListener = new AnimatorListenerAdapter() { 385 @Override 386 public void onAnimationEnd(Animator animation) { 387 if (!mIsWaitingForOtherPieceOfExitAnimation) { 388 finish(); 389 return; 390 } 391 mIsWaitingForOtherPieceOfExitAnimation = false; 392 } 393 }; 394 395 396 /** 397 * Data items are compared to the same mimetype based off of three qualities: 398 * 1. Super primary 399 * 2. Primary 400 * 3. Times used 401 */ 402 private final Comparator<DataItem> mWithinMimeTypeDataItemComparator = 403 new Comparator<DataItem>() { 404 @Override 405 public int compare(DataItem lhs, DataItem rhs) { 406 if (!lhs.getMimeType().equals(rhs.getMimeType())) { 407 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " + 408 lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType()); 409 return 0; 410 } 411 412 if (lhs.isSuperPrimary()) { 413 return -1; 414 } else if (rhs.isSuperPrimary()) { 415 return 1; 416 } else if (lhs.isPrimary() && !rhs.isPrimary()) { 417 return -1; 418 } else if (!lhs.isPrimary() && rhs.isPrimary()) { 419 return 1; 420 } else { 421 final int lhsTimesUsed = 422 lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 423 final int rhsTimesUsed = 424 rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 425 426 return rhsTimesUsed - lhsTimesUsed; 427 } 428 } 429 }; 430 431 private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator = 432 new Comparator<List<DataItem>> () { 433 @Override 434 public int compare(List<DataItem> lhsList, List<DataItem> rhsList) { 435 DataItem lhs = lhsList.get(0); 436 DataItem rhs = rhsList.get(0); 437 final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 438 final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 439 final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed; 440 if (timesUsedDifference != 0) { 441 return timesUsedDifference; 442 } 443 444 final long lhsLastTimeUsed = 445 lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed(); 446 final long rhsLastTimeUsed = 447 rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed(); 448 final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed; 449 if (lastTimeUsedDifference > 0) { 450 return 1; 451 } else if (lastTimeUsedDifference < 0) { 452 return -1; 453 } 454 455 // Times used and last time used are the same. Resort to statically defined. 456 final String lhsMimeType = lhs.getMimeType(); 457 final String rhsMimeType = rhs.getMimeType(); 458 for (String mimeType : LEADING_MIMETYPES) { 459 if (lhsMimeType.equals(mimeType)) { 460 return -1; 461 } else if (rhsMimeType.equals(mimeType)) { 462 return 1; 463 } 464 } 465 // Trailing types come last, so flip the returns 466 for (String mimeType : TRAILING_MIMETYPES) { 467 if (lhsMimeType.equals(mimeType)) { 468 return 1; 469 } else if (rhsMimeType.equals(mimeType)) { 470 return -1; 471 } 472 } 473 return 0; 474 } 475 }; 476 477 @Override 478 protected void onCreate(Bundle savedInstanceState) { 479 Trace.beginSection("onCreate()"); 480 super.onCreate(savedInstanceState); 481 482 getWindow().setStatusBarColor(Color.TRANSPARENT); 483 484 processIntent(getIntent()); 485 486 // Show QuickContact in front of soft input 487 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 488 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 489 490 setContentView(R.layout.quickcontact_activity); 491 492 mSmsComponent = PhoneCapabilityTester.getSmsComponent(this); 493 494 mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 495 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 496 mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card); 497 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 498 499 mContactCard.setOnClickListener(mEntryClickHandler); 500 mContactCard.setTitle(getResources().getString(R.string.communication_card_title)); 501 mContactCard.setExpandButtonText( 502 getResources().getString(R.string.expanding_entry_card_view_see_all)); 503 504 mRecentCard.setOnClickListener(mEntryClickHandler); 505 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 506 507 mAboutCard.setOnClickListener(mEntryClickHandler); 508 509 mPhotoView = (ImageView) findViewById(R.id.photo); 510 mTransparentView = findViewById(R.id.transparent_view); 511 if (mScroller != null) { 512 mTransparentView.setOnClickListener(new OnClickListener() { 513 @Override 514 public void onClick(View v) { 515 mScroller.scrollOffBottom(); 516 } 517 }); 518 } 519 520 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 521 setActionBar(toolbar); 522 getActionBar().setTitle(null); 523 // Put a TextView with a known resource id into the ActionBar. This allows us to easily 524 // find the correct TextView location & size later. 525 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null)); 526 527 mHasAlreadyBeenOpened = savedInstanceState != null; 528 529 mWindowScrim = new ColorDrawable(SCRIM_COLOR); 530 getWindow().setBackgroundDrawable(mWindowScrim); 531 if (!mHasAlreadyBeenOpened) { 532 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 533 ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 0xFF).setDuration(duration).start(); 534 } 535 536 mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED); 537 // mScroller needs to perform asynchronous measurements after initalize(), therefore 538 // we can't mark this as GONE. 539 mScroller.setVisibility(View.INVISIBLE); 540 541 setHeaderNameText(R.string.missing_name); 542 543 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 544 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 545 if (mSelectAccountFragmentListener == null) { 546 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 547 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 548 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 549 mSelectAccountFragmentListener.setRetainInstance(true); 550 } 551 mSelectAccountFragmentListener.setQuickContactActivity(this); 552 553 if (savedInstanceState != null) { 554 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0); 555 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 556 new Runnable() { 557 @Override 558 public void run() { 559 // Need to wait for the pre draw before setting the initial scroll 560 // value. Prior to pre draw all scroll values are invalid. 561 if (mHasAlreadyBeenOpened) { 562 mScroller.setVisibility(View.VISIBLE); 563 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 564 } 565 // Need to wait for pre draw for setting the theme color. Setting the 566 // header tint before the MultiShrinkScroller has been measured will 567 // cause incorrect tinting calculations. 568 if (color != 0) { 569 setThemeColor(MaterialColorMapUtils.calculateSecondaryColor(color)); 570 } 571 } 572 }); 573 } 574 575 Trace.endSection(); 576 } 577 578 protected void onActivityResult(int requestCode, int resultCode, 579 Intent data) { 580 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 581 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) { 582 // The contact that we were showing has been deleted. 583 finish(); 584 } 585 } 586 587 @Override 588 protected void onNewIntent(Intent intent) { 589 super.onNewIntent(intent); 590 mHasAlreadyBeenOpened = true; 591 mHasComputedThemeColor = false; 592 processIntent(intent); 593 } 594 595 @Override 596 public void onSaveInstanceState(Bundle savedInstanceState) { 597 super.onSaveInstanceState(savedInstanceState); 598 if (mColorFilter != null) { 599 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor()); 600 } 601 } 602 603 private void processIntent(Intent intent) { 604 Uri lookupUri = intent.getData(); 605 606 // Check to see whether it comes from the old version. 607 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 608 final long rawContactId = ContentUris.parseId(lookupUri); 609 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 610 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 611 } 612 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, 613 QuickContact.MODE_LARGE); 614 final Uri oldLookupUri = mLookupUri; 615 616 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 617 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 618 if (oldLookupUri == null) { 619 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 620 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 621 } else if (oldLookupUri != mLookupUri) { 622 // After copying a directory contact, the contact URI changes. Therefore, 623 // we need to restart the loader and reload the new contact. 624 mContactLoader = (ContactLoader) getLoaderManager().restartLoader( 625 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 626 for (int interactionLoaderId : mRecentLoaderIds) { 627 getLoaderManager().destroyLoader(interactionLoaderId); 628 } 629 } 630 631 NfcHandler.register(this, mLookupUri); 632 } 633 634 private void runEntranceAnimation() { 635 if (mHasAlreadyBeenOpened) { 636 return; 637 } 638 mHasAlreadyBeenOpened = true; 639 mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED); 640 } 641 642 /** Assign this string to the view if it is not empty. */ 643 private void setHeaderNameText(int resId) { 644 if (mScroller != null) { 645 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString()); 646 } 647 } 648 649 /** Assign this string to the view if it is not empty. */ 650 private void setHeaderNameText(String value) { 651 if (!TextUtils.isEmpty(value)) { 652 if (mScroller != null) { 653 mScroller.setTitle(value); 654 } 655 } 656 } 657 658 /** 659 * Check if the given MIME-type appears in the list of excluded MIME-types 660 * that the most-recent caller requested. 661 */ 662 private boolean isMimeExcluded(String mimeType) { 663 if (mExcludeMimes == null) return false; 664 for (String excludedMime : mExcludeMimes) { 665 if (TextUtils.equals(excludedMime, mimeType)) { 666 return true; 667 } 668 } 669 return false; 670 } 671 672 /** 673 * Handle the result from the ContactLoader 674 */ 675 private void bindContactData(final Contact data) { 676 Trace.beginSection("bindContactData"); 677 mContactData = data; 678 invalidateOptionsMenu(); 679 680 Trace.endSection(); 681 Trace.beginSection("Set display photo & name"); 682 683 mPhotoSetter.setupContactPhoto(data, mPhotoView); 684 extractAndApplyTintFromPhotoViewAsynchronously(); 685 analyzeWhitenessOfPhotoAsynchronously(); 686 setHeaderNameText(data.getDisplayName()); 687 688 Trace.endSection(); 689 690 mEntriesAndActionsTask = new AsyncTask<Void, Void, 691 Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>() { 692 693 @Override 694 protected Pair<List<List<DataItem>>, Map<String, List<DataItem>>> doInBackground( 695 Void... params) { 696 return generateDataModelFromContact(data); 697 } 698 699 @Override 700 protected void onPostExecute(Pair<List<List<DataItem>>, 701 Map<String, List<DataItem>>> dataItemsPair) { 702 super.onPostExecute(dataItemsPair); 703 mDataItemsList = dataItemsPair.first; 704 mDataItemsMap = dataItemsPair.second; 705 // Check that original AsyncTask parameters are still valid and the activity 706 // is still running before binding to UI. A new intent could invalidate 707 // the results, for example. 708 if (data == mContactData && !isCancelled()) { 709 bindDataToCards(); 710 showActivity(); 711 } 712 } 713 }; 714 mEntriesAndActionsTask.execute(); 715 } 716 717 private void bindDataToCards() { 718 startInteractionLoaders(); 719 populateContactAndAboutCard(); 720 } 721 722 private void startInteractionLoaders() { 723 final List<DataItem> phoneDataItems = mDataItemsMap.get(Phone.CONTENT_ITEM_TYPE); 724 String[] phoneNumbers = null; 725 if (phoneDataItems != null) { 726 phoneNumbers = new String[phoneDataItems.size()]; 727 for (int i = 0; i < phoneDataItems.size(); ++i) { 728 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber(); 729 } 730 } 731 final Bundle phonesExtraBundle = new Bundle(); 732 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers); 733 734 Trace.beginSection("start sms loader"); 735 getLoaderManager().initLoader( 736 LOADER_SMS_ID, 737 phonesExtraBundle, 738 mLoaderInteractionsCallbacks); 739 Trace.endSection(); 740 741 Trace.beginSection("start call log loader"); 742 getLoaderManager().initLoader( 743 LOADER_CALL_LOG_ID, 744 phonesExtraBundle, 745 mLoaderInteractionsCallbacks); 746 Trace.endSection(); 747 748 749 Trace.beginSection("start calendar loader"); 750 final List<DataItem> emailDataItems = mDataItemsMap.get(Email.CONTENT_ITEM_TYPE); 751 String[] emailAddresses = null; 752 if (emailDataItems != null) { 753 emailAddresses = new String[emailDataItems.size()]; 754 for (int i = 0; i < emailDataItems.size(); ++i) { 755 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress(); 756 } 757 } 758 final Bundle emailsExtraBundle = new Bundle(); 759 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses); 760 getLoaderManager().initLoader( 761 LOADER_CALENDAR_ID, 762 emailsExtraBundle, 763 mLoaderInteractionsCallbacks); 764 Trace.endSection(); 765 } 766 767 private void showActivity() { 768 if (mScroller != null) { 769 mScroller.setVisibility(View.VISIBLE); 770 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 771 new Runnable() { 772 @Override 773 public void run() { 774 runEntranceAnimation(); 775 } 776 }); 777 } 778 } 779 780 private void populateContactAndAboutCard() { 781 Trace.beginSection("bind contact card"); 782 783 final List<Entry> contactCardEntries = new ArrayList<>(); 784 final List<Entry> aboutCardEntries = new ArrayList<>(); 785 786 int topContactIndex = 0; 787 for (int i = 0; i < mDataItemsList.size(); ++i) { 788 final List<DataItem> dataItemsByMimeType = mDataItemsList.get(i); 789 final DataItem topDataItem = dataItemsByMimeType.get(0); 790 if (ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) { 791 aboutCardEntries.addAll(dataItemsToEntries(mDataItemsList.get(i))); 792 } else { 793 // Add most used to the top of the contact card 794 final Entry topEntry = dataItemToEntry(topDataItem); 795 if (topEntry != null) { 796 contactCardEntries.add(topContactIndex++, dataItemToEntry(topDataItem)); 797 } 798 // TODO merge SMS into secondary action 799 if (topDataItem instanceof PhoneDataItem) { 800 final PhoneDataItem phone = (PhoneDataItem) topDataItem; 801 Intent smsIntent = null; 802 if (mSmsComponent != null) { 803 smsIntent = new Intent(Intent.ACTION_SENDTO, 804 Uri.fromParts(CallUtil.SCHEME_SMSTO, phone.getNumber(), null)); 805 smsIntent.setComponent(mSmsComponent); 806 } 807 final int dataId = phone.getId() > Integer.MAX_VALUE ? 808 -1 : (int) phone.getId(); 809 contactCardEntries.add(topContactIndex++, 810 new Entry(dataId, 811 getResources().getDrawable(R.drawable.ic_message_24dp), 812 getResources().getString(R.string.send_message), 813 /* subHeader = */ null, 814 /* text = */ phone.buildDataString( 815 this, topDataItem.getDataKind()), 816 smsIntent, 817 /* isEditable = */ false)); 818 } 819 // Add the rest of the entries to the bottom of the card 820 if (dataItemsByMimeType.size() > 1) { 821 contactCardEntries.addAll(dataItemsToEntries( 822 dataItemsByMimeType.subList(1, dataItemsByMimeType.size()))); 823 } 824 } 825 } 826 827 if (contactCardEntries.size() > 0) { 828 mContactCard.initialize(contactCardEntries, 829 /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN, 830 /* isExpanded = */ false, 831 mExpandingEntryCardViewListener); 832 mContactCard.setVisibility(View.VISIBLE); 833 } else { 834 mContactCard.setVisibility(View.GONE); 835 } 836 Trace.endSection(); 837 838 Trace.beginSection("bind about card"); 839 mAboutCard.initialize(aboutCardEntries, 840 /* numInitialVisibleEntries = */ 1, 841 /* isExpanded = */ true, 842 mExpandingEntryCardViewListener); 843 Trace.endSection(); 844 } 845 846 /** 847 * Builds the {@link DataItem}s Map out of the Contact. 848 * @param data The contact to build the data from. 849 * @return A pair containing a list of data items sorted within mimetype and sorted 850 * amongst mimetype. The map goes from mimetype string to the sorted list of data items within 851 * mimetype 852 */ 853 private Pair<List<List<DataItem>>, Map<String, List<DataItem>>> generateDataModelFromContact( 854 Contact data) { 855 Trace.beginSection("Build data items map"); 856 857 final Map<String, List<DataItem>> dataItemsMap = new HashMap<>(); 858 859 final ResolveCache cache = ResolveCache.getInstance(this); 860 for (RawContact rawContact : data.getRawContacts()) { 861 for (DataItem dataItem : rawContact.getDataItems()) { 862 dataItem.setRawContactId(rawContact.getId()); 863 864 final String mimeType = dataItem.getMimeType(); 865 if (mimeType == null) continue; 866 867 final AccountType accountType = rawContact.getAccountType(this); 868 final DataKind dataKind = AccountTypeManager.getInstance(this) 869 .getKindOrFallback(accountType, mimeType); 870 if (dataKind == null) continue; 871 872 dataItem.setDataKind(dataKind); 873 874 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this, 875 dataKind)); 876 877 if (isMimeExcluded(mimeType) || !hasData) continue; 878 879 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType); 880 if (dataItemListByType == null) { 881 dataItemListByType = new ArrayList<>(); 882 dataItemsMap.put(mimeType, dataItemListByType); 883 } 884 dataItemListByType.add(dataItem); 885 } 886 } 887 Trace.endSection(); 888 889 Trace.beginSection("sort within mimetypes"); 890 /* 891 * Sorting is a multi part step. The end result is to a have a sorted list of the most 892 * used data items, one per mimetype. Then, within each mimetype, the list of data items 893 * for that type is also sorted, based off of {super primary, primary, times used} in that 894 * order. 895 */ 896 final List<List<DataItem>> dataItemsList = new ArrayList<>(); 897 for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) { 898 // Remove duplicate data items 899 Collapser.collapseList(mimeTypeDataItems, this); 900 // Sort within mimetype 901 Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator); 902 // Add to the list of data item lists 903 dataItemsList.add(mimeTypeDataItems); 904 } 905 Trace.endSection(); 906 907 Trace.beginSection("sort amongst mimetypes"); 908 // Sort amongst mimetypes to bubble up the top data items for the contact card 909 Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator); 910 Trace.endSection(); 911 912 return new Pair<>(dataItemsList, dataItemsMap); 913 } 914 915 /** 916 * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display. 917 * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned. 918 * @param dataItem The {@link DataItem} to convert. 919 * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present. 920 */ 921 private Entry dataItemToEntry(DataItem dataItem) { 922 Drawable icon = null; 923 String header = null; 924 String subHeader = null; 925 Drawable subHeaderIcon = null; 926 String text = null; 927 Drawable textIcon = null; 928 Intent intent = null; 929 final boolean isEditable = false; 930 931 DataKind kind = dataItem.getDataKind(); 932 933 if (dataItem instanceof ImDataItem) { 934 final ImDataItem im = (ImDataItem) dataItem; 935 intent = ContactsUtils.buildImIntent(this, im).first; 936 header = getResources().getString(R.string.header_im_entry); 937 final boolean isEmail = im.isCreatedFromEmail(); 938 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); 939 subHeader = Im.getProtocolLabel(getResources(), protocol, 940 im.getCustomProtocol()).toString(); 941 } else if (dataItem instanceof OrganizationDataItem) { 942 final OrganizationDataItem organization = (OrganizationDataItem) dataItem; 943 header = getResources().getString(R.string.header_organization_entry); 944 subHeader = organization.getCompany(); 945 text = organization.getTitle(); 946 } else if (dataItem instanceof NicknameDataItem) { 947 final NicknameDataItem nickname = (NicknameDataItem) dataItem; 948 // Build nickname entries 949 final boolean isNameRawContact = 950 (mContactData.getNameRawContactId() == dataItem.getRawContactId()); 951 952 final boolean duplicatesTitle = 953 isNameRawContact 954 && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 955 956 if (!duplicatesTitle) { 957 header = getResources().getString(R.string.header_nickname_entry); 958 subHeader = nickname.getName(); 959 } 960 } else if (dataItem instanceof NoteDataItem) { 961 final NoteDataItem note = (NoteDataItem) dataItem; 962 header = getResources().getString(R.string.header_note_entry); 963 subHeader = note.getNote(); 964 } else if (dataItem instanceof WebsiteDataItem) { 965 final WebsiteDataItem website = (WebsiteDataItem) dataItem; 966 header = getResources().getString(R.string.header_website_entry); 967 subHeader = website.getUrl(); 968 try { 969 final WebAddress webAddress = new WebAddress(website.buildDataString(this, kind)); 970 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString())); 971 } catch (final ParseException e) { 972 Log.e(TAG, "Couldn't parse website: " + website.buildDataString(this, kind)); 973 } 974 } else if (dataItem instanceof EventDataItem) { 975 final EventDataItem event = (EventDataItem) dataItem; 976 final String dataString = event.buildDataString(this, kind); 977 final Calendar cal = DateUtils.parseDate(dataString, false); 978 if (cal != null) { 979 final Date nextAnniversary = 980 DateUtils.getNextAnnualDate(cal); 981 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 982 builder.appendPath("time"); 983 ContentUris.appendId(builder, nextAnniversary.getTime()); 984 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); 985 } 986 header = getResources().getString(R.string.header_event_entry); 987 if (event.hasKindTypeColumn(kind)) { 988 subHeader = getResources().getString(Event.getTypeResource( 989 event.getKindTypeColumn(kind))); 990 } 991 text = DateUtils.formatDate(this, dataString); 992 } else if (dataItem instanceof RelationDataItem) { 993 final RelationDataItem relation = (RelationDataItem) dataItem; 994 final String dataString = relation.buildDataString(this, kind); 995 if (!TextUtils.isEmpty(dataString)) { 996 intent = new Intent(Intent.ACTION_SEARCH); 997 intent.putExtra(SearchManager.QUERY, dataString); 998 intent.setType(Contacts.CONTENT_TYPE); 999 } 1000 header = getResources().getString(R.string.header_relation_entry); 1001 subHeader = relation.getName(); 1002 if (relation.hasKindTypeColumn(kind)) { 1003 text = Relation.getTypeLabel(getResources(), relation.getKindTypeColumn(kind), 1004 relation.getLabel()).toString(); 1005 } 1006 } else if (dataItem instanceof PhoneDataItem) { 1007 final PhoneDataItem phone = (PhoneDataItem) dataItem; 1008 if (!TextUtils.isEmpty(phone.getNumber())) { 1009 header = phone.buildDataString(this, kind); 1010 if (phone.hasKindTypeColumn(kind)) { 1011 text = Phone.getTypeLabel(getResources(), phone.getKindTypeColumn(kind), 1012 phone.getLabel()).toString(); 1013 } 1014 icon = getResources().getDrawable(R.drawable.ic_phone_24dp); 1015 if (PhoneCapabilityTester.isPhone(this)) { 1016 intent = CallUtil.getCallIntent(phone.getNumber()); 1017 } 1018 } 1019 } else if (dataItem instanceof EmailDataItem) { 1020 final EmailDataItem email = (EmailDataItem) dataItem; 1021 final String address = email.getData(); 1022 if (!TextUtils.isEmpty(address)) { 1023 final Uri mailUri = Uri.fromParts(CallUtil.SCHEME_MAILTO, address, null); 1024 intent = new Intent(Intent.ACTION_SENDTO, mailUri); 1025 header = email.getAddress(); 1026 if (email.hasKindTypeColumn(kind)) { 1027 text = Email.getTypeLabel(getResources(), email.getKindTypeColumn(kind), 1028 email.getLabel()).toString(); 1029 } 1030 icon = getResources().getDrawable(R.drawable.ic_email_24dp); 1031 } 1032 } else if (dataItem instanceof StructuredPostalDataItem) { 1033 StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem; 1034 final String postalAddress = postal.getFormattedAddress(); 1035 if (!TextUtils.isEmpty(postalAddress)) { 1036 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress); 1037 header = postal.getFormattedAddress(); 1038 if (postal.hasKindTypeColumn(kind)) { 1039 text = StructuredPostal.getTypeLabel(getResources(), 1040 postal.getKindTypeColumn(kind), postal.getLabel()).toString(); 1041 } 1042 icon = getResources().getDrawable(R.drawable.ic_place_24dp); 1043 } 1044 } else if (dataItem instanceof SipAddressDataItem) { 1045 if (PhoneCapabilityTester.isSipPhone(this)) { 1046 final SipAddressDataItem sip = (SipAddressDataItem) dataItem; 1047 final String address = sip.getSipAddress(); 1048 if (!TextUtils.isEmpty(address)) { 1049 final Uri callUri = Uri.fromParts(CallUtil.SCHEME_SIP, address, null); 1050 intent = CallUtil.getCallIntent(callUri); 1051 // Note that this item will get a SIP-specific variant 1052 // of the "call phone" icon, rather than the standard 1053 // app icon for the Phone app (which we show for 1054 // regular phone numbers.) That's because the phone 1055 // app explicitly specifies an android:icon attribute 1056 // for the SIP-related intent-filters in its manifest. 1057 } 1058 icon = ResolveCache.getInstance(this).getIcon(sip.getMimeType(), intent); 1059 // Call mutate to create a new Drawable.ConstantState for color filtering 1060 if (icon != null) { 1061 icon.mutate(); 1062 } 1063 } 1064 } else if (dataItem instanceof StructuredNameDataItem) { 1065 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName(); 1066 if (!TextUtils.isEmpty(givenName)) { 1067 mAboutCard.setTitle(getResources().getString(R.string.about_card_title) + 1068 " " + givenName); 1069 } else { 1070 mAboutCard.setTitle(getResources().getString(R.string.about_card_title)); 1071 } 1072 } else { 1073 // Custom DataItem 1074 header = dataItem.buildDataStringForDisplay(this, kind); 1075 text = kind.typeColumn; 1076 intent = new Intent(Intent.ACTION_VIEW); 1077 intent.setDataAndType(Uri.parse(dataItem.buildDataString(this, kind)), 1078 dataItem.getMimeType()); 1079 icon = ResolveCache.getInstance(this).getIcon(dataItem.getMimeType(), intent); 1080 } 1081 1082 if (intent != null) { 1083 // Do not set the intent is there are no resolves 1084 if (!PhoneCapabilityTester.isIntentRegistered(this, intent)) { 1085 intent = null; 1086 } 1087 } 1088 1089 // If the Entry has no visual elements, return null 1090 if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) && 1091 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) { 1092 return null; 1093 } 1094 1095 final int dataId = dataItem.getId() > Integer.MAX_VALUE ? 1096 -1 : (int) dataItem.getId(); 1097 1098 return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon, 1099 intent, isEditable); 1100 } 1101 1102 private List<Entry> dataItemsToEntries(List<DataItem> dataItems) { 1103 final List<Entry> entries = new ArrayList<>(); 1104 for (DataItem dataItem : dataItems) { 1105 final Entry entry = dataItemToEntry(dataItem); 1106 if (entry != null) { 1107 entries.add(entry); 1108 } 1109 } 1110 return entries; 1111 } 1112 1113 /** 1114 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 1115 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 1116 * on a Nexus 5. 1117 */ 1118 private void extractAndApplyTintFromPhotoViewAsynchronously() { 1119 if (mScroller == null) { 1120 return; 1121 } 1122 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1123 new AsyncTask<Void, Void, MaterialPalette>() { 1124 @Override 1125 protected MaterialPalette doInBackground(Void... params) { 1126 1127 if (imageViewDrawable instanceof BitmapDrawable) { 1128 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 1129 final int primaryColor = colorFromBitmap(bitmap); 1130 if (primaryColor != 0) { 1131 return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 1132 primaryColor); 1133 } 1134 } 1135 if (imageViewDrawable instanceof LetterTileDrawable) { 1136 final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor(); 1137 return MaterialColorMapUtils.calculateSecondaryColor(primaryColor); 1138 } 1139 return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 1140 getResources().getColor(R.color.quickcontact_default_photo_tint_color)); 1141 } 1142 1143 @Override 1144 protected void onPostExecute(MaterialPalette palette) { 1145 super.onPostExecute(palette); 1146 if (mHasComputedThemeColor) { 1147 // If we had previously computed a theme color from the contact photo, 1148 // then do not update the theme color. Changing the theme color several 1149 // seconds after QC has started, as a result of an updated/upgraded photo, 1150 // is a jarring experience. On the other hand, changing the theme color after 1151 // a rotation or onNewIntent() is perfectly fine. 1152 return; 1153 } 1154 // Check that the Photo has not changed. If it has changed, the new tint 1155 // color needs to be extracted 1156 if (imageViewDrawable == mPhotoView.getDrawable()) { 1157 mHasComputedThemeColor = true; 1158 setThemeColor(palette); 1159 } 1160 } 1161 }.execute(); 1162 } 1163 1164 /** 1165 * Examine how many white pixels are in the bitmap in order to determine whether or not 1166 * we need gradient overlays on top of the image. 1167 */ 1168 private void analyzeWhitenessOfPhotoAsynchronously() { 1169 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1170 new AsyncTask<Void, Void, Boolean>() { 1171 @Override 1172 protected Boolean doInBackground(Void... params) { 1173 if (imageViewDrawable instanceof BitmapDrawable) { 1174 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 1175 return WhitenessUtils.isBitmapWhiteAtTopOrBottom(bitmap); 1176 } 1177 return !(imageViewDrawable instanceof LetterTileDrawable); 1178 } 1179 1180 @Override 1181 protected void onPostExecute(Boolean isWhite) { 1182 super.onPostExecute(isWhite); 1183 mScroller.setUseGradient(isWhite); 1184 } 1185 }.execute(); 1186 } 1187 1188 private void setThemeColor(MaterialPalette palette) { 1189 // If the color is invalid, use the predefined default 1190 final int primaryColor = palette.mPrimaryColor; 1191 mScroller.setHeaderTintColor(primaryColor); 1192 mStatusBarColor = palette.mSecondaryColor; 1193 updateStatusBarColor(); 1194 1195 mColorFilter = 1196 new PorterDuffColorFilter(primaryColor, PorterDuff.Mode.SRC_ATOP); 1197 mContactCard.setColorAndFilter(primaryColor, mColorFilter); 1198 mRecentCard.setColorAndFilter(primaryColor, mColorFilter); 1199 mAboutCard.setColorAndFilter(primaryColor, mColorFilter); 1200 } 1201 1202 private void updateStatusBarColor() { 1203 if (mScroller == null) { 1204 return; 1205 } 1206 final int desiredStatusBarColor; 1207 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 1208 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 1209 desiredStatusBarColor = mStatusBarColor; 1210 } else { 1211 desiredStatusBarColor = Color.TRANSPARENT; 1212 } 1213 // Animate to the new color. 1214 if (desiredStatusBarColor != getWindow().getStatusBarColor()) { 1215 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 1216 getWindow().getStatusBarColor(), desiredStatusBarColor); 1217 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 1218 animation.setEvaluator(new ArgbEvaluator()); 1219 animation.start(); 1220 } 1221 } 1222 1223 private int colorFromBitmap(Bitmap bitmap) { 1224 // Author of Palette recommends using 24 colors when analyzing profile photos. 1225 final int NUMBER_OF_PALETTE_COLORS = 24; 1226 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 1227 if (palette != null && palette.getVibrantSwatch() != null) { 1228 return palette.getVibrantSwatch().getRgb(); 1229 } 1230 return 0; 1231 } 1232 1233 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 1234 final List<Entry> entries = new ArrayList<>(); 1235 for (ContactInteraction interaction : interactions) { 1236 entries.add(new Entry(/* id = */ -1, 1237 interaction.getIcon(this), 1238 interaction.getViewHeader(this), 1239 interaction.getViewBody(this), 1240 interaction.getBodyIcon(this), 1241 interaction.getViewFooter(this), 1242 interaction.getFooterIcon(this), 1243 interaction.getIntent(), 1244 /* isEditable = */ false)); 1245 } 1246 return entries; 1247 } 1248 1249 private final LoaderCallbacks<Contact> mLoaderContactCallbacks = 1250 new LoaderCallbacks<Contact>() { 1251 @Override 1252 public void onLoaderReset(Loader<Contact> loader) { 1253 } 1254 1255 @Override 1256 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1257 Trace.beginSection("onLoadFinished()"); 1258 1259 if (isFinishing()) { 1260 return; 1261 } 1262 if (data.isError()) { 1263 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 1264 // should log the actual exception. 1265 throw new IllegalStateException("Failed to load contact", data.getException()); 1266 } 1267 if (data.isNotFound()) { 1268 if (mHasAlreadyBeenOpened) { 1269 finish(); 1270 } else { 1271 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 1272 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 1273 Toast.LENGTH_LONG).show(); 1274 } 1275 return; 1276 } 1277 1278 bindContactData(data); 1279 1280 Trace.endSection(); 1281 } 1282 1283 @Override 1284 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1285 if (mLookupUri == null) { 1286 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 1287 } 1288 // Load all contact data. We need loadGroupMetaData=true to determine whether the 1289 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 1290 return new ContactLoader(getApplicationContext(), mLookupUri, 1291 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 1292 true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 1293 } 1294 }; 1295 1296 @Override 1297 public void onBackPressed() { 1298 if (mScroller != null) { 1299 if (!mIsExitAnimationInProgress) { 1300 mScroller.scrollOffBottom(); 1301 } 1302 } else { 1303 super.onBackPressed(); 1304 } 1305 } 1306 1307 @Override 1308 public void finish() { 1309 super.finish(); 1310 1311 // override transitions to skip the standard window animations 1312 overridePendingTransition(0, 0); 1313 } 1314 1315 private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 1316 new LoaderCallbacks<List<ContactInteraction>>() { 1317 1318 @Override 1319 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 1320 Log.v(TAG, "onCreateLoader"); 1321 Loader<List<ContactInteraction>> loader = null; 1322 switch (id) { 1323 case LOADER_SMS_ID: 1324 Log.v(TAG, "LOADER_SMS_ID"); 1325 loader = new SmsInteractionsLoader( 1326 QuickContactActivity.this, 1327 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1328 MAX_SMS_RETRIEVE); 1329 break; 1330 case LOADER_CALENDAR_ID: 1331 Log.v(TAG, "LOADER_CALENDAR_ID"); 1332 final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS); 1333 List<String> emailsList = null; 1334 if (emailsArray != null) { 1335 emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)); 1336 } 1337 loader = new CalendarInteractionsLoader( 1338 QuickContactActivity.this, 1339 emailsList, 1340 MAX_FUTURE_CALENDAR_RETRIEVE, 1341 MAX_PAST_CALENDAR_RETRIEVE, 1342 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 1343 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 1344 break; 1345 case LOADER_CALL_LOG_ID: 1346 Log.v(TAG, "LOADER_CALL_LOG_ID"); 1347 loader = new CallLogInteractionsLoader( 1348 QuickContactActivity.this, 1349 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1350 MAX_CALL_LOG_RETRIEVE); 1351 } 1352 return loader; 1353 } 1354 1355 @Override 1356 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 1357 List<ContactInteraction> data) { 1358 if (mRecentLoaderResults == null) { 1359 mRecentLoaderResults = new HashMap<Integer, List<ContactInteraction>>(); 1360 } 1361 Log.v(TAG, "onLoadFinished ~ loader.getId() " + loader.getId() + " data.size() " + 1362 data.size()); 1363 mRecentLoaderResults.put(loader.getId(), data); 1364 1365 if (isAllRecentDataLoaded()) { 1366 bindRecentData(); 1367 } 1368 } 1369 1370 @Override 1371 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 1372 mRecentLoaderResults.remove(loader.getId()); 1373 } 1374 1375 }; 1376 1377 private boolean isAllRecentDataLoaded() { 1378 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 1379 } 1380 1381 private void bindRecentData() { 1382 final List<ContactInteraction> allInteractions = new ArrayList<>(); 1383 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 1384 allInteractions.addAll(loaderInteractions); 1385 } 1386 1387 // Sort the interactions by most recent 1388 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 1389 @Override 1390 public int compare(ContactInteraction a, ContactInteraction b) { 1391 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1; 1392 } 1393 }); 1394 1395 if (allInteractions.size() > 0) { 1396 mRecentCard.initialize(contactInteractionsToEntries(allInteractions), 1397 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 1398 /* isExpanded = */ false, mExpandingEntryCardViewListener); 1399 mRecentCard.setVisibility(View.VISIBLE); 1400 } 1401 1402 // About card is initialized along with the contact card, but since it appears after 1403 // the recent card in the UI, we hold off until making it visible until the recent card 1404 // is also ready to avoid stuttering. 1405 if (mAboutCard.shouldShow()) { 1406 mAboutCard.setVisibility(View.VISIBLE); 1407 } else { 1408 mAboutCard.setVisibility(View.GONE); 1409 } 1410 } 1411 1412 @Override 1413 protected void onStop() { 1414 super.onStop(); 1415 1416 if (mEntriesAndActionsTask != null) { 1417 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 1418 // results on the UI thread. In some circumstances Activities are killed without 1419 // onStop() being called. This is not a problem, because in these circumstances 1420 // the entire process will be killed. 1421 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 1422 } 1423 } 1424 1425 /** 1426 * Returns true if it is possible to edit the current contact. 1427 */ 1428 private boolean isContactEditable() { 1429 return mContactData != null && !mContactData.isDirectoryEntry(); 1430 } 1431 1432 private void editContact() { 1433 final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); 1434 mContactLoader.cacheResult(); 1435 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1436 startActivityForResult(intent, REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 1437 } 1438 1439 private void toggleStar(MenuItem starredMenuItem) { 1440 // Make sure there is a contact 1441 if (mLookupUri != null) { 1442 // Read the current starred value from the UI instead of using the last 1443 // loaded state. This allows rapid tapping without writing the same 1444 // value several times 1445 final boolean isStarred = starredMenuItem.isChecked(); 1446 1447 // To improve responsiveness, swap out the picture (and tag) in the UI already 1448 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1449 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1450 !isStarred); 1451 1452 // Now perform the real save 1453 final Intent intent = ContactSaveService.createSetStarredIntent( 1454 QuickContactActivity.this, mLookupUri, !isStarred); 1455 startService(intent); 1456 } 1457 } 1458 1459 /** 1460 * Calls into the contacts provider to get a pre-authorized version of the given URI. 1461 */ 1462 private Uri getPreAuthorizedUri(Uri uri) { 1463 final Bundle uriBundle = new Bundle(); 1464 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri); 1465 final Bundle authResponse = getContentResolver().call( 1466 ContactsContract.AUTHORITY_URI, 1467 ContactsContract.Authorization.AUTHORIZATION_METHOD, 1468 null, 1469 uriBundle); 1470 if (authResponse != null) { 1471 return (Uri) authResponse.getParcelable( 1472 ContactsContract.Authorization.KEY_AUTHORIZED_URI); 1473 } else { 1474 return uri; 1475 } 1476 } 1477 1478 private void shareContact() { 1479 final String lookupKey = mContactData.getLookupKey(); 1480 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 1481 if (mContactData.isUserProfile()) { 1482 // User is sharing the profile. We don't want to force the receiver to have 1483 // the highly-privileged READ_PROFILE permission, so we need to request a 1484 // pre-authorized URI from the provider. 1485 shareUri = getPreAuthorizedUri(shareUri); 1486 } 1487 1488 final Intent intent = new Intent(Intent.ACTION_SEND); 1489 intent.setType(Contacts.CONTENT_VCARD_TYPE); 1490 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 1491 1492 // Launch chooser to share contact via 1493 final CharSequence chooseTitle = getText(R.string.share_via); 1494 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 1495 1496 try { 1497 this.startActivity(chooseIntent); 1498 } catch (final ActivityNotFoundException ex) { 1499 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1500 } 1501 } 1502 1503 /** 1504 * Creates a launcher shortcut with the current contact. 1505 */ 1506 private void createLauncherShortcutWithContact() { 1507 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 1508 new OnShortcutIntentCreatedListener() { 1509 1510 @Override 1511 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 1512 // Broadcast the shortcutIntent to the launcher to create a 1513 // shortcut to this contact 1514 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 1515 QuickContactActivity.this.sendBroadcast(shortcutIntent); 1516 1517 // Send a toast to give feedback to the user that a shortcut to this 1518 // contact was added to the launcher. 1519 Toast.makeText(QuickContactActivity.this, 1520 R.string.createContactShortcutSuccessful, 1521 Toast.LENGTH_SHORT).show(); 1522 } 1523 1524 }); 1525 builder.createContactShortcutIntent(mLookupUri); 1526 } 1527 1528 @Override 1529 public boolean onCreateOptionsMenu(Menu menu) { 1530 final MenuInflater inflater = getMenuInflater(); 1531 inflater.inflate(R.menu.quickcontact, menu); 1532 return true; 1533 } 1534 1535 @Override 1536 public boolean onPrepareOptionsMenu(Menu menu) { 1537 if (mContactData != null) { 1538 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 1539 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1540 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1541 mContactData.getStarred()); 1542 // Configure edit MenuItem 1543 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 1544 editMenuItem.setVisible(true); 1545 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 1546 .isInvisibleAndAddable(mContactData, this)) { 1547 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp); 1548 } else if (isContactEditable()) { 1549 editMenuItem.setIcon(R.drawable.ic_create_24dp); 1550 } else { 1551 editMenuItem.setVisible(false); 1552 } 1553 return true; 1554 } 1555 return false; 1556 } 1557 1558 @Override 1559 public boolean onOptionsItemSelected(MenuItem item) { 1560 switch (item.getItemId()) { 1561 case R.id.menu_star: 1562 toggleStar(item); 1563 return true; 1564 case R.id.menu_edit: 1565 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 1566 DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(), 1567 mSelectAccountFragmentListener); 1568 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1569 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 1570 } else if (isContactEditable()) { 1571 editContact(); 1572 } 1573 return true; 1574 case R.id.menu_share: 1575 shareContact(); 1576 return true; 1577 case R.id.menu_create_contact_shortcut: 1578 createLauncherShortcutWithContact(); 1579 return true; 1580 default: 1581 return super.onOptionsItemSelected(item); 1582 } 1583 } 1584} 1585