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