QuickContactActivity.java revision 48290bed7c6a8bd5e7be8b206dddacf9047a945f
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.accounts.Account; 20import android.animation.ArgbEvaluator; 21import android.animation.ObjectAnimator; 22import android.app.Activity; 23import android.app.Fragment; 24import android.app.LoaderManager.LoaderCallbacks; 25import android.app.SearchManager; 26import android.content.ActivityNotFoundException; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.Intent; 31import android.content.Loader; 32import android.content.pm.PackageManager; 33import android.content.pm.ResolveInfo; 34import android.content.res.Resources; 35import android.graphics.Bitmap; 36import android.graphics.BitmapFactory; 37import android.graphics.Color; 38import android.graphics.PorterDuff; 39import android.graphics.PorterDuffColorFilter; 40import android.graphics.drawable.BitmapDrawable; 41import android.graphics.drawable.ColorDrawable; 42import android.graphics.drawable.Drawable; 43import android.net.ParseException; 44import android.net.Uri; 45import android.net.WebAddress; 46import android.os.AsyncTask; 47import android.os.Bundle; 48import android.os.Trace; 49import android.provider.CalendarContract; 50import android.provider.ContactsContract; 51import android.provider.ContactsContract.CommonDataKinds.Email; 52import android.provider.ContactsContract.CommonDataKinds.Event; 53import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 54import android.provider.ContactsContract.CommonDataKinds.Identity; 55import android.provider.ContactsContract.CommonDataKinds.Im; 56import android.provider.ContactsContract.CommonDataKinds.Nickname; 57import android.provider.ContactsContract.CommonDataKinds.Note; 58import android.provider.ContactsContract.CommonDataKinds.Organization; 59import android.provider.ContactsContract.CommonDataKinds.Phone; 60import android.provider.ContactsContract.CommonDataKinds.Relation; 61import android.provider.ContactsContract.CommonDataKinds.SipAddress; 62import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 63import android.provider.ContactsContract.CommonDataKinds.Website; 64import android.provider.ContactsContract.Contacts; 65import android.provider.ContactsContract.Data; 66import android.provider.ContactsContract.Directory; 67import android.provider.ContactsContract.DisplayNameSources; 68import android.provider.ContactsContract.DataUsageFeedback; 69import android.provider.ContactsContract.Intents; 70import android.provider.ContactsContract.QuickContact; 71import android.provider.ContactsContract.RawContacts; 72import android.support.v7.graphics.Palette; 73import android.telecomm.PhoneAccount; 74import android.telecomm.TelecommManager; 75import android.text.TextUtils; 76import android.util.Log; 77import android.view.ContextMenu; 78import android.view.ContextMenu.ContextMenuInfo; 79import android.view.Menu; 80import android.view.MenuInflater; 81import android.view.MenuItem; 82import android.view.MotionEvent; 83import android.view.View; 84import android.view.View.OnClickListener; 85import android.view.View.OnCreateContextMenuListener; 86import android.view.WindowManager; 87import android.widget.Toast; 88import android.widget.Toolbar; 89 90import com.android.contacts.ContactSaveService; 91import com.android.contacts.ContactsActivity; 92import com.android.contacts.NfcHandler; 93import com.android.contacts.R; 94import com.android.contacts.common.CallUtil; 95import com.android.contacts.common.ClipboardUtils; 96import com.android.contacts.common.Collapser; 97import com.android.contacts.common.ContactsUtils; 98import com.android.contacts.common.editor.SelectAccountDialogFragment; 99import com.android.contacts.common.interactions.TouchPointManager; 100import com.android.contacts.common.lettertiles.LetterTileDrawable; 101import com.android.contacts.common.list.ShortcutIntentBuilder; 102import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 103import com.android.contacts.common.model.AccountTypeManager; 104import com.android.contacts.common.model.Contact; 105import com.android.contacts.common.model.ContactLoader; 106import com.android.contacts.common.model.RawContact; 107import com.android.contacts.common.model.account.AccountType; 108import com.android.contacts.common.model.account.AccountWithDataSet; 109import com.android.contacts.common.model.dataitem.DataItem; 110import com.android.contacts.common.model.dataitem.DataKind; 111import com.android.contacts.common.model.dataitem.EmailDataItem; 112import com.android.contacts.common.model.dataitem.EventDataItem; 113import com.android.contacts.common.model.dataitem.ImDataItem; 114import com.android.contacts.common.model.dataitem.NicknameDataItem; 115import com.android.contacts.common.model.dataitem.NoteDataItem; 116import com.android.contacts.common.model.dataitem.OrganizationDataItem; 117import com.android.contacts.common.model.dataitem.PhoneDataItem; 118import com.android.contacts.common.model.dataitem.RelationDataItem; 119import com.android.contacts.common.model.dataitem.SipAddressDataItem; 120import com.android.contacts.common.model.dataitem.StructuredNameDataItem; 121import com.android.contacts.common.model.dataitem.StructuredPostalDataItem; 122import com.android.contacts.common.model.dataitem.WebsiteDataItem; 123import com.android.contacts.common.util.DateUtils; 124import com.android.contacts.common.util.MaterialColorMapUtils; 125import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 126import com.android.contacts.common.util.ViewUtil; 127import com.android.contacts.detail.ContactDisplayUtils; 128import com.android.contacts.interactions.CalendarInteractionsLoader; 129import com.android.contacts.interactions.CallLogInteractionsLoader; 130import com.android.contacts.interactions.ContactDeletionInteraction; 131import com.android.contacts.interactions.ContactInteraction; 132import com.android.contacts.interactions.SmsInteractionsLoader; 133import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 134import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryContextMenuInfo; 135import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryTag; 136import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener; 137import com.android.contacts.util.ImageViewDrawableSetter; 138import com.android.contacts.util.PhoneCapabilityTester; 139import com.android.contacts.util.SchedulingUtils; 140import com.android.contacts.util.StructuredPostalUtils; 141import com.android.contacts.widget.MultiShrinkScroller; 142import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 143import com.android.contacts.widget.QuickContactImageView; 144 145import com.google.common.base.Preconditions; 146import com.google.common.collect.Lists; 147 148import java.util.ArrayList; 149import java.util.Arrays; 150import java.util.Calendar; 151import java.util.Collections; 152import java.util.Comparator; 153import java.util.Date; 154import java.util.HashMap; 155import java.util.List; 156import java.util.Map; 157 158/** 159 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 160 * data asynchronously, and then shows a popup with details centered around 161 * {@link Intent#getSourceBounds()}. 162 */ 163public class QuickContactActivity extends ContactsActivity { 164 165 /** 166 * QuickContacts immediately takes up the full screen. All possible information is shown. 167 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 168 * should only be used by the Contacts app. 169 */ 170 public static final int MODE_FULLY_EXPANDED = 4; 171 172 private static final String TAG = "QuickContact"; 173 174 private static final String KEY_THEME_COLOR = "theme_color"; 175 176 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150; 177 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 178 private static final int SCRIM_COLOR = Color.argb(0xC8, 0, 0, 0); 179 private static final int REQUEST_CODE_CONTACT_SELECTION_ACTIVITY = 2; 180 private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms"; 181 182 /** This is the Intent action to install a shortcut in the launcher. */ 183 private static final String ACTION_INSTALL_SHORTCUT = 184 "com.android.launcher.action.INSTALL_SHORTCUT"; 185 186 @SuppressWarnings("deprecation") 187 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 188 189 private static final String MIMETYPE_GPLUS_PROFILE = 190 "vnd.android.cursor.item/vnd.googleplus.profile"; 191 private static final String INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE = "Add to circle"; 192 private static final String MIMETYPE_HANGOUTS = 193 "vnd.android.cursor.item/vnd.googleplus.profile.comm"; 194 private static final String INTENT_DATA_HANGOUTS_VIDEO = "Start video call"; 195 private static final String CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY = 196 "com.android.contacts.quickcontact.QuickContactActivity"; 197 198 /** 199 * The URI used to load the the Contact. Once the contact is loaded, use Contact#getLookupUri() 200 * instead of referencing this URI. 201 */ 202 private Uri mLookupUri; 203 private String[] mExcludeMimes; 204 private int mExtraMode; 205 private int mStatusBarColor; 206 private boolean mHasAlreadyBeenOpened; 207 208 private QuickContactImageView mPhotoView; 209 private ExpandingEntryCardView mContactCard; 210 private ExpandingEntryCardView mNoContactDetailsCard; 211 private ExpandingEntryCardView mRecentCard; 212 private ExpandingEntryCardView mAboutCard; 213 private MultiShrinkScroller mScroller; 214 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 215 private AsyncTask<Void, Void, Cp2DataCardModel> mEntriesAndActionsTask; 216 private AsyncTask<Void, Void, Void> mRecentDataTask; 217 /** 218 * The last copy of Cp2DataCardModel that was passed to {@link #populateContactAndAboutCard}. 219 */ 220 private Cp2DataCardModel mCachedCp2DataCardModel; 221 /** 222 * This scrim's opacity is controlled in two different ways. 1) Before the initial entrance 223 * animation finishes, the opacity is animated by a value animator. This is designed to 224 * distract the user from the length of the initial loading time. 2) After the initial 225 * entrance animation, the opacity is directly related to scroll position. 226 */ 227 private ColorDrawable mWindowScrim; 228 private boolean mIsEntranceAnimationFinished; 229 private MaterialColorMapUtils mMaterialColorMapUtils; 230 private boolean mIsExitAnimationInProgress; 231 private boolean mHasComputedThemeColor; 232 233 /** 234 * Used to stop the ExpandingEntry cards from adjusting between an entry click and the intent 235 * being launched. 236 */ 237 private boolean mHasIntentLaunched; 238 239 private Contact mContactData; 240 private ContactLoader mContactLoader; 241 private PorterDuffColorFilter mColorFilter; 242 243 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 244 245 /** 246 * {@link #LEADING_MIMETYPES} is used to sort MIME-types. 247 * 248 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 249 * in the order specified here.</p> 250 */ 251 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 252 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, 253 StructuredPostal.CONTENT_ITEM_TYPE); 254 255 private static final List<String> SORTED_ABOUT_CARD_MIMETYPES = Lists.newArrayList( 256 Nickname.CONTENT_ITEM_TYPE, 257 // Phonetic name is inserted after nickname if it is available. 258 // No mimetype for phonetic name exists. 259 Website.CONTENT_ITEM_TYPE, 260 Organization.CONTENT_ITEM_TYPE, 261 Event.CONTENT_ITEM_TYPE, 262 Relation.CONTENT_ITEM_TYPE, 263 Im.CONTENT_ITEM_TYPE, 264 GroupMembership.CONTENT_ITEM_TYPE, 265 Identity.CONTENT_ITEM_TYPE, 266 Note.CONTENT_ITEM_TYPE); 267 268 /** Id for the background contact loader */ 269 private static final int LOADER_CONTACT_ID = 0; 270 271 private static final String KEY_LOADER_EXTRA_PHONES = 272 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES"; 273 274 /** Id for the background Sms Loader */ 275 private static final int LOADER_SMS_ID = 1; 276 private static final int MAX_SMS_RETRIEVE = 3; 277 278 /** Id for the back Calendar Loader */ 279 private static final int LOADER_CALENDAR_ID = 2; 280 private static final String KEY_LOADER_EXTRA_EMAILS = 281 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS"; 282 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 283 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 284 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 285 1L * 24L * 60L * 60L * 1000L /* 1 day */; 286 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 287 7L * 24L * 60L * 60L * 1000L /* 7 days */; 288 289 /** Id for the background Call Log Loader */ 290 private static final int LOADER_CALL_LOG_ID = 3; 291 private static final int MAX_CALL_LOG_RETRIEVE = 3; 292 private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3; 293 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 294 private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2; 295 296 297 private static final int[] mRecentLoaderIds = new int[]{ 298 LOADER_SMS_ID, 299 LOADER_CALENDAR_ID, 300 LOADER_CALL_LOG_ID}; 301 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults = new HashMap<>(); 302 303 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 304 305 final OnClickListener mEntryClickHandler = new OnClickListener() { 306 @Override 307 public void onClick(View v) { 308 final Object entryTagObject = v.getTag(); 309 if (entryTagObject == null || !(entryTagObject instanceof EntryTag)) { 310 Log.w(TAG, "EntryTag was not used correctly"); 311 return; 312 } 313 final EntryTag entryTag = (EntryTag) entryTagObject; 314 final Intent intent = entryTag.getIntent(); 315 final int dataId = entryTag.getId(); 316 317 if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) { 318 editContact(); 319 return; 320 } 321 322 // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id 323 // so the exact usage type is not necessary in all cases 324 String usageType = DataUsageFeedback.USAGE_TYPE_CALL; 325 326 final Uri intentUri = intent.getData(); 327 if ((intentUri != null && intentUri.getScheme() != null && 328 intentUri.getScheme().equals(ContactsUtils.SCHEME_SMSTO)) || 329 (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) { 330 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT; 331 } 332 333 // Data IDs start at 1 so anything less is invalid 334 if (dataId > 0) { 335 final Uri dataUsageUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() 336 .appendPath(String.valueOf(dataId)) 337 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType) 338 .build(); 339 final boolean successful = getContentResolver().update( 340 dataUsageUri, new ContentValues(), null, null) > 0; 341 if (!successful) { 342 Log.w(TAG, "DataUsageFeedback increment failed"); 343 } 344 } else { 345 Log.w(TAG, "Invalid Data ID"); 346 } 347 348 // Pass the touch point through the intent for use in the InCallUI 349 if (Intent.ACTION_CALL.equals(intent.getAction())) { 350 if (TouchPointManager.getInstance().hasValidPoint()) { 351 Bundle extras = new Bundle(); 352 extras.putParcelable(TouchPointManager.TOUCH_POINT, 353 TouchPointManager.getInstance().getPoint()); 354 intent.putExtra(TelecommManager.EXTRA_OUTGOING_CALL_EXTRAS, extras); 355 } 356 } 357 358 mHasIntentLaunched = true; 359 startActivity(intent); 360 } 361 }; 362 363 final ExpandingEntryCardViewListener mExpandingEntryCardViewListener 364 = new ExpandingEntryCardViewListener() { 365 @Override 366 public void onCollapse(int heightDelta) { 367 mScroller.prepareForShrinkingScrollChild(heightDelta); 368 } 369 370 @Override 371 public void onExpand(int heightDelta) { 372 mScroller.prepareForExpandingScrollChild(); 373 } 374 }; 375 376 private final OnCreateContextMenuListener mEntryContextMenuListener = 377 new OnCreateContextMenuListener() { 378 @Override 379 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 380 if (menuInfo == null) { 381 return; 382 } 383 EntryContextMenuInfo info = (EntryContextMenuInfo) menuInfo; 384 menu.setHeaderTitle(info.getCopyText()); 385 menu.add(R.string.copy_text); 386 } 387 }; 388 389 @Override 390 public boolean onContextItemSelected(MenuItem item) { 391 EntryContextMenuInfo menuInfo; 392 try { 393 menuInfo = (EntryContextMenuInfo) item.getMenuInfo(); 394 } catch (ClassCastException e) { 395 Log.e(TAG, "bad menuInfo", e); 396 return false; 397 } 398 399 ClipboardUtils.copyText(this, menuInfo.getCopyLabel(), menuInfo.getCopyText(), true); 400 return true; 401 } 402 403 /** 404 * Headless fragment used to handle account selection callbacks invoked from 405 * {@link DirectoryContactUtil}. 406 */ 407 public static class SelectAccountDialogFragmentListener extends Fragment 408 implements SelectAccountDialogFragment.Listener { 409 410 private QuickContactActivity mQuickContactActivity; 411 412 public SelectAccountDialogFragmentListener() {} 413 414 @Override 415 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 416 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 417 account, mQuickContactActivity); 418 } 419 420 @Override 421 public void onAccountSelectorCancelled() {} 422 423 /** 424 * Set the parent activity. Since rotation can cause this fragment to be used across 425 * more than one activity instance, we need to explicitly set this value instead 426 * of making this class non-static. 427 */ 428 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 429 mQuickContactActivity = quickContactActivity; 430 } 431 } 432 433 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 434 = new MultiShrinkScrollerListener() { 435 @Override 436 public void onScrolledOffBottom() { 437 finish(); 438 } 439 440 @Override 441 public void onEnterFullscreen() { 442 updateStatusBarColor(); 443 } 444 445 @Override 446 public void onExitFullscreen() { 447 updateStatusBarColor(); 448 } 449 450 @Override 451 public void onStartScrollOffBottom() { 452 mIsExitAnimationInProgress = true; 453 } 454 455 @Override 456 public void onEntranceAnimationDone() { 457 mIsEntranceAnimationFinished = true; 458 } 459 460 @Override 461 public void onTransparentViewHeightChange(float ratio) { 462 if (mIsEntranceAnimationFinished) { 463 mWindowScrim.setAlpha((int) (0xFF * ratio)); 464 } 465 } 466 }; 467 468 469 /** 470 * Data items are compared to the same mimetype based off of three qualities: 471 * 1. Super primary 472 * 2. Primary 473 * 3. Times used 474 */ 475 private final Comparator<DataItem> mWithinMimeTypeDataItemComparator = 476 new Comparator<DataItem>() { 477 @Override 478 public int compare(DataItem lhs, DataItem rhs) { 479 if (!lhs.getMimeType().equals(rhs.getMimeType())) { 480 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " + 481 lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType()); 482 return 0; 483 } 484 485 if (lhs.isSuperPrimary()) { 486 return -1; 487 } else if (rhs.isSuperPrimary()) { 488 return 1; 489 } else if (lhs.isPrimary() && !rhs.isPrimary()) { 490 return -1; 491 } else if (!lhs.isPrimary() && rhs.isPrimary()) { 492 return 1; 493 } else { 494 final int lhsTimesUsed = 495 lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 496 final int rhsTimesUsed = 497 rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 498 499 return rhsTimesUsed - lhsTimesUsed; 500 } 501 } 502 }; 503 504 /** 505 * Sorts among different mimetypes based off: 506 * 1. Times used 507 * 2. Last time used 508 * 3. Statically defined 509 */ 510 private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator = 511 new Comparator<List<DataItem>> () { 512 @Override 513 public int compare(List<DataItem> lhsList, List<DataItem> rhsList) { 514 DataItem lhs = lhsList.get(0); 515 DataItem rhs = rhsList.get(0); 516 final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 517 final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 518 final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed; 519 if (timesUsedDifference != 0) { 520 return timesUsedDifference; 521 } 522 523 final long lhsLastTimeUsed = 524 lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed(); 525 final long rhsLastTimeUsed = 526 rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed(); 527 final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed; 528 if (lastTimeUsedDifference > 0) { 529 return 1; 530 } else if (lastTimeUsedDifference < 0) { 531 return -1; 532 } 533 534 // Times used and last time used are the same. Resort to statically defined. 535 final String lhsMimeType = lhs.getMimeType(); 536 final String rhsMimeType = rhs.getMimeType(); 537 for (String mimeType : LEADING_MIMETYPES) { 538 if (lhsMimeType.equals(mimeType)) { 539 return -1; 540 } else if (rhsMimeType.equals(mimeType)) { 541 return 1; 542 } 543 } 544 return 0; 545 } 546 }; 547 548 @Override 549 public boolean dispatchTouchEvent(MotionEvent ev) { 550 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 551 TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); 552 } 553 return super.dispatchTouchEvent(ev); 554 } 555 556 @Override 557 protected void onCreate(Bundle savedInstanceState) { 558 Trace.beginSection("onCreate()"); 559 super.onCreate(savedInstanceState); 560 561 getWindow().setStatusBarColor(Color.TRANSPARENT); 562 563 processIntent(getIntent()); 564 565 // Show QuickContact in front of soft input 566 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 567 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 568 569 setContentView(R.layout.quickcontact_activity); 570 571 mMaterialColorMapUtils = new MaterialColorMapUtils(getResources()); 572 573 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 574 575 mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 576 mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card); 577 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 578 mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card); 579 580 mNoContactDetailsCard.setOnClickListener(mEntryClickHandler); 581 mContactCard.setOnClickListener(mEntryClickHandler); 582 mContactCard.setExpandButtonText( 583 getResources().getString(R.string.expanding_entry_card_view_see_all)); 584 mContactCard.setOnCreateContextMenuListener(mEntryContextMenuListener); 585 586 mRecentCard.setOnClickListener(mEntryClickHandler); 587 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 588 589 mAboutCard.setOnClickListener(mEntryClickHandler); 590 mAboutCard.setOnCreateContextMenuListener(mEntryContextMenuListener); 591 592 mPhotoView = (QuickContactImageView) findViewById(R.id.photo); 593 final View transparentView = findViewById(R.id.transparent_view); 594 if (mScroller != null) { 595 transparentView.setOnClickListener(new OnClickListener() { 596 @Override 597 public void onClick(View v) { 598 mScroller.scrollOffBottom(); 599 } 600 }); 601 } 602 603 // Allow a shadow to be shown under the toolbar. 604 ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources()); 605 606 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 607 setActionBar(toolbar); 608 getActionBar().setTitle(null); 609 // Put a TextView with a known resource id into the ActionBar. This allows us to easily 610 // find the correct TextView location & size later. 611 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null)); 612 613 mHasAlreadyBeenOpened = savedInstanceState != null; 614 mIsEntranceAnimationFinished = mHasAlreadyBeenOpened; 615 mWindowScrim = new ColorDrawable(SCRIM_COLOR); 616 mWindowScrim.setAlpha(0); 617 getWindow().setBackgroundDrawable(mWindowScrim); 618 619 mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED); 620 // mScroller needs to perform asynchronous measurements after initalize(), therefore 621 // we can't mark this as GONE. 622 mScroller.setVisibility(View.INVISIBLE); 623 624 setHeaderNameText(R.string.missing_name); 625 626 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 627 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 628 if (mSelectAccountFragmentListener == null) { 629 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 630 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 631 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 632 mSelectAccountFragmentListener.setRetainInstance(true); 633 } 634 mSelectAccountFragmentListener.setQuickContactActivity(this); 635 636 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true, 637 new Runnable() { 638 @Override 639 public void run() { 640 if (!mHasAlreadyBeenOpened) { 641 // The initial scrim opacity must match the scrim opacity that would be 642 // achieved by scrolling to the starting position. 643 final float alphaRatio = mExtraMode == MODE_FULLY_EXPANDED ? 644 1 : mScroller.getStartingTransparentHeightRatio(); 645 final int duration = getResources().getInteger( 646 android.R.integer.config_shortAnimTime); 647 final int desiredAlpha = (int) (0xFF * alphaRatio); 648 ObjectAnimator o = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 649 desiredAlpha).setDuration(duration); 650 651 o.start(); 652 } 653 } 654 }); 655 656 if (savedInstanceState != null) { 657 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0); 658 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 659 new Runnable() { 660 @Override 661 public void run() { 662 // Need to wait for the pre draw before setting the initial scroll 663 // value. Prior to pre draw all scroll values are invalid. 664 if (mHasAlreadyBeenOpened) { 665 mScroller.setVisibility(View.VISIBLE); 666 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 667 } 668 // Need to wait for pre draw for setting the theme color. Setting the 669 // header tint before the MultiShrinkScroller has been measured will 670 // cause incorrect tinting calculations. 671 if (color != 0) { 672 setThemeColor(mMaterialColorMapUtils 673 .calculatePrimaryAndSecondaryColor(color)); 674 } 675 } 676 }); 677 } 678 679 Trace.endSection(); 680 } 681 682 @Override 683 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 684 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 685 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) { 686 // The contact that we were showing has been deleted. 687 finish(); 688 } else if (requestCode == REQUEST_CODE_CONTACT_SELECTION_ACTIVITY && 689 resultCode != RESULT_CANCELED) { 690 processIntent(data); 691 } 692 } 693 694 @Override 695 protected void onNewIntent(Intent intent) { 696 super.onNewIntent(intent); 697 mHasAlreadyBeenOpened = true; 698 mIsEntranceAnimationFinished = true; 699 mHasComputedThemeColor = false; 700 processIntent(intent); 701 } 702 703 @Override 704 public void onSaveInstanceState(Bundle savedInstanceState) { 705 super.onSaveInstanceState(savedInstanceState); 706 if (mColorFilter != null) { 707 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor()); 708 } 709 } 710 711 private void processIntent(Intent intent) { 712 Uri lookupUri = intent.getData(); 713 714 // Check to see whether it comes from the old version. 715 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 716 final long rawContactId = ContentUris.parseId(lookupUri); 717 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 718 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 719 } 720 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, 721 QuickContact.MODE_LARGE); 722 final Uri oldLookupUri = mLookupUri; 723 724 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 725 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 726 if (oldLookupUri == null) { 727 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 728 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 729 } else if (oldLookupUri != mLookupUri) { 730 // After copying a directory contact, the contact URI changes. Therefore, 731 // we need to restart the loader and reload the new contact. 732 for (int interactionLoaderId : mRecentLoaderIds) { 733 getLoaderManager().destroyLoader(interactionLoaderId); 734 } 735 mContactLoader = (ContactLoader) getLoaderManager().restartLoader( 736 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 737 } 738 739 NfcHandler.register(this, mLookupUri); 740 } 741 742 private void runEntranceAnimation() { 743 if (mHasAlreadyBeenOpened) { 744 return; 745 } 746 mHasAlreadyBeenOpened = true; 747 mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED); 748 } 749 750 /** Assign this string to the view if it is not empty. */ 751 private void setHeaderNameText(int resId) { 752 if (mScroller != null) { 753 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString()); 754 } 755 } 756 757 /** Assign this string to the view if it is not empty. */ 758 private void setHeaderNameText(String value) { 759 if (!TextUtils.isEmpty(value)) { 760 if (mScroller != null) { 761 mScroller.setTitle(value); 762 } 763 } 764 } 765 766 /** 767 * Check if the given MIME-type appears in the list of excluded MIME-types 768 * that the most-recent caller requested. 769 */ 770 private boolean isMimeExcluded(String mimeType) { 771 if (mExcludeMimes == null) return false; 772 for (String excludedMime : mExcludeMimes) { 773 if (TextUtils.equals(excludedMime, mimeType)) { 774 return true; 775 } 776 } 777 return false; 778 } 779 780 /** 781 * Handle the result from the ContactLoader 782 */ 783 private void bindContactData(final Contact data) { 784 Trace.beginSection("bindContactData"); 785 mContactData = data; 786 invalidateOptionsMenu(); 787 788 Trace.endSection(); 789 Trace.beginSection("Set display photo & name"); 790 791 mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization()); 792 mPhotoSetter.setupContactPhoto(data, mPhotoView); 793 extractAndApplyTintFromPhotoViewAsynchronously(); 794 analyzeWhitenessOfPhotoAsynchronously(); 795 setHeaderNameText(ContactDisplayUtils.getDisplayName(this, data).toString()); 796 797 Trace.endSection(); 798 799 mEntriesAndActionsTask = new AsyncTask<Void, Void, Cp2DataCardModel>() { 800 801 @Override 802 protected Cp2DataCardModel doInBackground( 803 Void... params) { 804 return generateDataModelFromContact(data); 805 } 806 807 @Override 808 protected void onPostExecute(Cp2DataCardModel cardDataModel) { 809 super.onPostExecute(cardDataModel); 810 // Check that original AsyncTask parameters are still valid and the activity 811 // is still running before binding to UI. A new intent could invalidate 812 // the results, for example. 813 if (data == mContactData && !isCancelled()) { 814 bindDataToCards(cardDataModel); 815 showActivity(); 816 } 817 } 818 }; 819 mEntriesAndActionsTask.execute(); 820 } 821 822 private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) { 823 startInteractionLoaders(cp2DataCardModel); 824 populateContactAndAboutCard(cp2DataCardModel); 825 } 826 827 private void startInteractionLoaders(Cp2DataCardModel cp2DataCardModel) { 828 final Map<String, List<DataItem>> dataItemsMap = cp2DataCardModel.dataItemsMap; 829 final List<DataItem> phoneDataItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE); 830 String[] phoneNumbers = null; 831 if (phoneDataItems != null) { 832 phoneNumbers = new String[phoneDataItems.size()]; 833 for (int i = 0; i < phoneDataItems.size(); ++i) { 834 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber(); 835 } 836 } 837 final Bundle phonesExtraBundle = new Bundle(); 838 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers); 839 840 Trace.beginSection("start sms loader"); 841 getLoaderManager().initLoader( 842 LOADER_SMS_ID, 843 phonesExtraBundle, 844 mLoaderInteractionsCallbacks); 845 Trace.endSection(); 846 847 Trace.beginSection("start call log loader"); 848 getLoaderManager().initLoader( 849 LOADER_CALL_LOG_ID, 850 phonesExtraBundle, 851 mLoaderInteractionsCallbacks); 852 Trace.endSection(); 853 854 855 Trace.beginSection("start calendar loader"); 856 final List<DataItem> emailDataItems = dataItemsMap.get(Email.CONTENT_ITEM_TYPE); 857 String[] emailAddresses = null; 858 if (emailDataItems != null) { 859 emailAddresses = new String[emailDataItems.size()]; 860 for (int i = 0; i < emailDataItems.size(); ++i) { 861 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress(); 862 } 863 } 864 final Bundle emailsExtraBundle = new Bundle(); 865 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses); 866 getLoaderManager().initLoader( 867 LOADER_CALENDAR_ID, 868 emailsExtraBundle, 869 mLoaderInteractionsCallbacks); 870 Trace.endSection(); 871 } 872 873 private void showActivity() { 874 if (mScroller != null) { 875 mScroller.setVisibility(View.VISIBLE); 876 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 877 new Runnable() { 878 @Override 879 public void run() { 880 runEntranceAnimation(); 881 } 882 }); 883 } 884 } 885 886 private List<List<Entry>> buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap) { 887 final List<List<Entry>> aboutCardEntries = new ArrayList<>(); 888 for (String mimetype : SORTED_ABOUT_CARD_MIMETYPES) { 889 final List<DataItem> mimeTypeItems = dataItemsMap.get(mimetype); 890 if (mimeTypeItems == null) { 891 continue; 892 } 893 // Set aboutCardTitleOut = null, since SORTED_ABOUT_CARD_MIMETYPES doesn't contain 894 // the name mimetype. 895 final List<Entry> aboutEntries = dataItemsToEntries(mimeTypeItems, 896 /* aboutCardTitleOut = */ null); 897 if (aboutEntries.size() > 0) { 898 aboutCardEntries.add(aboutEntries); 899 } 900 } 901 return aboutCardEntries; 902 } 903 904 @Override 905 protected void onResume() { 906 super.onResume(); 907 // If returning from a launched activity, repopulate the contact and about card 908 if (mHasIntentLaunched) { 909 mHasIntentLaunched = false; 910 populateContactAndAboutCard(mCachedCp2DataCardModel); 911 } 912 } 913 914 private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel) { 915 mCachedCp2DataCardModel = cp2DataCardModel; 916 if (mHasIntentLaunched || cp2DataCardModel == null) { 917 return; 918 } 919 Trace.beginSection("bind contact card"); 920 921 final List<List<Entry>> contactCardEntries = cp2DataCardModel.contactCardEntries; 922 final List<List<Entry>> aboutCardEntries = cp2DataCardModel.aboutCardEntries; 923 final String customAboutCardName = cp2DataCardModel.customAboutCardName; 924 925 if (contactCardEntries.size() > 0) { 926 mContactCard.initialize(contactCardEntries, 927 /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN, 928 /* isExpanded = */ mContactCard.isExpanded(), 929 /* isAlwaysExpanded = */ false, 930 mExpandingEntryCardViewListener, 931 mScroller); 932 mContactCard.setVisibility(View.VISIBLE); 933 } else { 934 mContactCard.setVisibility(View.GONE); 935 } 936 Trace.endSection(); 937 938 Trace.beginSection("bind about card"); 939 // Phonetic name is not a data item, so the entry needs to be created separately 940 final String phoneticName = mContactData.getPhoneticName(); 941 if (!TextUtils.isEmpty(phoneticName)) { 942 Entry phoneticEntry = new Entry(/* viewId = */ -1, 943 /* icon = */ null, 944 getResources().getString(R.string.name_phonetic), 945 phoneticName, 946 /* text = */ null, 947 /* primaryContentDescription = */ null, 948 /* intent = */ null, 949 /* alternateIcon = */ null, 950 /* alternateIntent = */ null, 951 /* alternateContentDescription = */ null, 952 /* shouldApplyColor = */ false, 953 /* isEditable = */ false, 954 /* EntryContextMenuInfo = */ new EntryContextMenuInfo(phoneticName, 955 getResources().getString(R.string.name_phonetic)), 956 /* thirdIcon = */ null, 957 /* thirdIntent = */ null, 958 /* thirdContentDescription = */ null, 959 /* iconResourceId = */ 0); 960 List<Entry> phoneticList = new ArrayList<>(); 961 phoneticList.add(phoneticEntry); 962 // Phonetic name comes after nickname. Check to see if the first entry type is nickname 963 if (aboutCardEntries.size() > 0 && aboutCardEntries.get(0).get(0).getHeader().equals( 964 getResources().getString(R.string.header_nickname_entry))) { 965 aboutCardEntries.add(1, phoneticList); 966 } else { 967 aboutCardEntries.add(0, phoneticList); 968 } 969 } 970 971 if (!TextUtils.isEmpty(customAboutCardName)) { 972 mAboutCard.setTitle(customAboutCardName); 973 } 974 975 if (aboutCardEntries.size() > 0) { 976 mAboutCard.initialize(aboutCardEntries, 977 /* numInitialVisibleEntries = */ 1, 978 /* isExpanded = */ true, 979 /* isAlwaysExpanded = */ true, 980 mExpandingEntryCardViewListener, 981 mScroller); 982 } 983 984 if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) { 985 initializeNoContactDetailCard(); 986 } else { 987 mNoContactDetailsCard.setVisibility(View.GONE); 988 } 989 990 // If the Recent card is already initialized (all recent data is loaded), show the About 991 // card if it has entries. Otherwise About card visibility will be set in bindRecentData() 992 if (isAllRecentDataLoaded() && aboutCardEntries.size() > 0) { 993 mAboutCard.setVisibility(View.VISIBLE); 994 } 995 Trace.endSection(); 996 } 997 998 /** 999 * Create a card that shows "Add email" and "Add phone number" entries in grey. 1000 */ 1001 private void initializeNoContactDetailCard() { 1002 final Drawable phoneIcon = getResources().getDrawable( 1003 R.drawable.ic_phone_24dp).mutate(); 1004 final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 1005 phoneIcon, getString(R.string.quickcontact_add_phone_number), 1006 /* subHeader = */ null, /* text = */ null, /* primaryContentDescription = */ null, 1007 getEditContactIntent(), 1008 /* alternateIcon = */ null, /* alternateIntent = */ null, 1009 /* alternateContentDescription = */ null, /* shouldApplyColor = */ true, 1010 /* isEditable = */ false, /* EntryContextMenuInfo = */ null, 1011 /* thirdIcon = */ null, /* thirdIntent = */ null, 1012 /* thirdContentDescription = */ null, R.drawable.ic_phone_24dp); 1013 1014 final Drawable emailIcon = getResources().getDrawable( 1015 R.drawable.ic_email_24dp).mutate(); 1016 final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 1017 emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null, 1018 /* text = */ null, /* primaryContentDescription = */ null, 1019 getEditContactIntent(), /* alternateIcon = */ null, 1020 /* alternateIntent = */ null, /* alternateContentDescription = */ null, 1021 /* shouldApplyColor = */ true, /* isEditable = */ false, 1022 /* EntryContextMenuInfo = */ null, /* thirdIcon = */ null, 1023 /* thirdIntent = */ null, /* thirdContentDescription = */ null, 1024 R.drawable.ic_email_24dp); 1025 1026 final List<List<Entry>> promptEntries = new ArrayList<>(); 1027 promptEntries.add(new ArrayList<Entry>(1)); 1028 promptEntries.add(new ArrayList<Entry>(1)); 1029 promptEntries.get(0).add(phonePromptEntry); 1030 promptEntries.get(1).add(emailPromptEntry); 1031 1032 final int subHeaderTextColor = getResources().getColor( 1033 R.color.quickcontact_entry_sub_header_text_color); 1034 final PorterDuffColorFilter greyColorFilter = 1035 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP); 1036 mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ true, 1037 /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener, mScroller); 1038 mNoContactDetailsCard.setVisibility(View.VISIBLE); 1039 mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor); 1040 mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter); 1041 } 1042 1043 /** 1044 * Builds the {@link DataItem}s Map out of the Contact. 1045 * @param data The contact to build the data from. 1046 * @return A pair containing a list of data items sorted within mimetype and sorted 1047 * amongst mimetype. The map goes from mimetype string to the sorted list of data items within 1048 * mimetype 1049 */ 1050 private Cp2DataCardModel generateDataModelFromContact( 1051 Contact data) { 1052 Trace.beginSection("Build data items map"); 1053 1054 final Map<String, List<DataItem>> dataItemsMap = new HashMap<>(); 1055 1056 final ResolveCache cache = ResolveCache.getInstance(this); 1057 for (RawContact rawContact : data.getRawContacts()) { 1058 for (DataItem dataItem : rawContact.getDataItems()) { 1059 dataItem.setRawContactId(rawContact.getId()); 1060 1061 final String mimeType = dataItem.getMimeType(); 1062 if (mimeType == null) continue; 1063 1064 final AccountType accountType = rawContact.getAccountType(this); 1065 final DataKind dataKind = AccountTypeManager.getInstance(this) 1066 .getKindOrFallback(accountType, mimeType); 1067 if (dataKind == null) continue; 1068 1069 dataItem.setDataKind(dataKind); 1070 1071 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this, 1072 dataKind)); 1073 1074 if (isMimeExcluded(mimeType) || !hasData) continue; 1075 1076 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType); 1077 if (dataItemListByType == null) { 1078 dataItemListByType = new ArrayList<>(); 1079 dataItemsMap.put(mimeType, dataItemListByType); 1080 } 1081 dataItemListByType.add(dataItem); 1082 } 1083 } 1084 Trace.endSection(); 1085 1086 Trace.beginSection("sort within mimetypes"); 1087 /* 1088 * Sorting is a multi part step. The end result is to a have a sorted list of the most 1089 * used data items, one per mimetype. Then, within each mimetype, the list of data items 1090 * for that type is also sorted, based off of {super primary, primary, times used} in that 1091 * order. 1092 */ 1093 final List<List<DataItem>> dataItemsList = new ArrayList<>(); 1094 for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) { 1095 // Remove duplicate data items 1096 Collapser.collapseList(mimeTypeDataItems, this); 1097 // Sort within mimetype 1098 Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator); 1099 // Add to the list of data item lists 1100 dataItemsList.add(mimeTypeDataItems); 1101 } 1102 Trace.endSection(); 1103 1104 Trace.beginSection("sort amongst mimetypes"); 1105 // Sort amongst mimetypes to bubble up the top data items for the contact card 1106 Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator); 1107 Trace.endSection(); 1108 1109 Trace.beginSection("cp2 data items to entries"); 1110 1111 final List<List<Entry>> contactCardEntries = new ArrayList<>(); 1112 final List<List<Entry>> aboutCardEntries = buildAboutCardEntries(dataItemsMap); 1113 final MutableString aboutCardName = new MutableString(); 1114 1115 for (int i = 0; i < dataItemsList.size(); ++i) { 1116 final List<DataItem> dataItemsByMimeType = dataItemsList.get(i); 1117 final DataItem topDataItem = dataItemsByMimeType.get(0); 1118 if (SORTED_ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) { 1119 // About card mimetypes are built in buildAboutCardEntries, skip here 1120 continue; 1121 } else { 1122 List<Entry> contactEntries = dataItemsToEntries(dataItemsList.get(i), 1123 aboutCardName); 1124 if (contactEntries.size() > 0) { 1125 contactCardEntries.add(contactEntries); 1126 } 1127 } 1128 } 1129 1130 Trace.endSection(); 1131 1132 final Cp2DataCardModel dataModel = new Cp2DataCardModel(); 1133 dataModel.customAboutCardName = aboutCardName.value; 1134 dataModel.aboutCardEntries = aboutCardEntries; 1135 dataModel.contactCardEntries = contactCardEntries; 1136 dataModel.dataItemsMap = dataItemsMap; 1137 return dataModel; 1138 } 1139 1140 /** 1141 * Class used to hold the About card and Contact cards' data model that gets generated 1142 * on a background thread. All data is from CP2. 1143 */ 1144 private static class Cp2DataCardModel { 1145 /** 1146 * A map between a mimetype string and the corresponding list of data items. The data items 1147 * are in sorted order using mWithinMimeTypeDataItemComparator. 1148 */ 1149 public Map<String, List<DataItem>> dataItemsMap; 1150 public List<List<Entry>> aboutCardEntries; 1151 public List<List<Entry>> contactCardEntries; 1152 public String customAboutCardName; 1153 } 1154 1155 private static class MutableString { 1156 public String value; 1157 } 1158 1159 /** 1160 * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display. 1161 * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned. 1162 * 1163 * This runs on a background thread. This is set as static to avoid accidentally adding 1164 * additional dependencies on unsafe things (like the Activity). 1165 * 1166 * @param dataItem The {@link DataItem} to convert. 1167 * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present. 1168 */ 1169 private static Entry dataItemToEntry(DataItem dataItem, 1170 Context context, Contact contactData, 1171 final MutableString aboutCardName) { 1172 Drawable icon = null; 1173 String header = null; 1174 String subHeader = null; 1175 Drawable subHeaderIcon = null; 1176 String text = null; 1177 Drawable textIcon = null; 1178 StringBuilder primaryContentDescription = new StringBuilder(); 1179 Intent intent = null; 1180 boolean shouldApplyColor = true; 1181 Drawable alternateIcon = null; 1182 Intent alternateIntent = null; 1183 StringBuilder alternateContentDescription = new StringBuilder(); 1184 final boolean isEditable = false; 1185 EntryContextMenuInfo entryContextMenuInfo = null; 1186 Drawable thirdIcon = null; 1187 Intent thirdIntent = null; 1188 String thirdContentDescription = null; 1189 int iconResourceId = 0; 1190 1191 context = context.getApplicationContext(); 1192 final Resources res = context.getResources(); 1193 DataKind kind = dataItem.getDataKind(); 1194 1195 if (dataItem instanceof ImDataItem) { 1196 final ImDataItem im = (ImDataItem) dataItem; 1197 intent = ContactsUtils.buildImIntent(context, im).first; 1198 final boolean isEmail = im.isCreatedFromEmail(); 1199 final int protocol; 1200 if (!im.isProtocolValid()) { 1201 protocol = Im.PROTOCOL_CUSTOM; 1202 } else { 1203 protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); 1204 } 1205 if (protocol == Im.PROTOCOL_CUSTOM) { 1206 // If the protocol is custom, display the "IM" entry header as well to distinguish 1207 // this entry from other ones 1208 header = res.getString(R.string.header_im_entry); 1209 subHeader = Im.getProtocolLabel(res, protocol, 1210 im.getCustomProtocol()).toString(); 1211 text = im.getData(); 1212 } else { 1213 header = Im.getProtocolLabel(res, protocol, 1214 im.getCustomProtocol()).toString(); 1215 subHeader = im.getData(); 1216 } 1217 entryContextMenuInfo = new EntryContextMenuInfo(im.getData(), header); 1218 } else if (dataItem instanceof OrganizationDataItem) { 1219 final OrganizationDataItem organization = (OrganizationDataItem) dataItem; 1220 header = res.getString(R.string.header_organization_entry); 1221 subHeader = organization.getCompany(); 1222 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header); 1223 text = organization.getTitle(); 1224 } else if (dataItem instanceof NicknameDataItem) { 1225 final NicknameDataItem nickname = (NicknameDataItem) dataItem; 1226 // Build nickname entries 1227 final boolean isNameRawContact = 1228 (contactData.getNameRawContactId() == dataItem.getRawContactId()); 1229 1230 final boolean duplicatesTitle = 1231 isNameRawContact 1232 && contactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 1233 1234 if (!duplicatesTitle) { 1235 header = res.getString(R.string.header_nickname_entry); 1236 subHeader = nickname.getName(); 1237 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header); 1238 } 1239 } else if (dataItem instanceof NoteDataItem) { 1240 final NoteDataItem note = (NoteDataItem) dataItem; 1241 header = res.getString(R.string.header_note_entry); 1242 subHeader = note.getNote(); 1243 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header); 1244 } else if (dataItem instanceof WebsiteDataItem) { 1245 final WebsiteDataItem website = (WebsiteDataItem) dataItem; 1246 header = res.getString(R.string.header_website_entry); 1247 subHeader = website.getUrl(); 1248 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header); 1249 try { 1250 final WebAddress webAddress = new WebAddress(website.buildDataString(context, 1251 kind)); 1252 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString())); 1253 } catch (final ParseException e) { 1254 Log.e(TAG, "Couldn't parse website: " + website.buildDataString(context, kind)); 1255 } 1256 } else if (dataItem instanceof EventDataItem) { 1257 final EventDataItem event = (EventDataItem) dataItem; 1258 final String dataString = event.buildDataString(context, kind); 1259 final Calendar cal = DateUtils.parseDate(dataString, false); 1260 if (cal != null) { 1261 final Date nextAnniversary = 1262 DateUtils.getNextAnnualDate(cal); 1263 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 1264 builder.appendPath("time"); 1265 ContentUris.appendId(builder, nextAnniversary.getTime()); 1266 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); 1267 } 1268 header = res.getString(R.string.header_event_entry); 1269 if (event.hasKindTypeColumn(kind)) { 1270 subHeader = Event.getTypeLabel(res, event.getKindTypeColumn(kind), 1271 event.getLabel()).toString(); 1272 } 1273 text = DateUtils.formatDate(context, dataString); 1274 entryContextMenuInfo = new EntryContextMenuInfo(text, header); 1275 } else if (dataItem instanceof RelationDataItem) { 1276 final RelationDataItem relation = (RelationDataItem) dataItem; 1277 final String dataString = relation.buildDataString(context, kind); 1278 if (!TextUtils.isEmpty(dataString)) { 1279 intent = new Intent(Intent.ACTION_SEARCH); 1280 intent.putExtra(SearchManager.QUERY, dataString); 1281 intent.setType(Contacts.CONTENT_TYPE); 1282 } 1283 header = res.getString(R.string.header_relation_entry); 1284 subHeader = relation.getName(); 1285 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header); 1286 if (relation.hasKindTypeColumn(kind)) { 1287 text = Relation.getTypeLabel(res, 1288 relation.getKindTypeColumn(kind), 1289 relation.getLabel()).toString(); 1290 } 1291 } else if (dataItem instanceof PhoneDataItem) { 1292 final PhoneDataItem phone = (PhoneDataItem) dataItem; 1293 if (!TextUtils.isEmpty(phone.getNumber())) { 1294 primaryContentDescription.append(res.getString(R.string.call_other)).append(" "); 1295 header = phone.buildDataString(context, kind); 1296 entryContextMenuInfo = new EntryContextMenuInfo(header, 1297 res.getString(R.string.phoneLabelsGroup)); 1298 if (phone.hasKindTypeColumn(kind)) { 1299 text = Phone.getTypeLabel(res, phone.getKindTypeColumn(kind), 1300 phone.getLabel()).toString(); 1301 primaryContentDescription.append(text).append(" "); 1302 } 1303 primaryContentDescription.append(header); 1304 icon = res.getDrawable(R.drawable.ic_phone_24dp); 1305 iconResourceId = R.drawable.ic_phone_24dp; 1306 if (PhoneCapabilityTester.isPhone(context)) { 1307 intent = CallUtil.getCallIntent(phone.getNumber()); 1308 } 1309 alternateIntent = new Intent(Intent.ACTION_SENDTO, 1310 Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phone.getNumber(), null)); 1311 1312 alternateIcon = res.getDrawable(R.drawable.ic_message_24dp); 1313 alternateContentDescription.append(res.getString(R.string.sms_custom, header)); 1314 1315 // Add video call button if supported 1316 if (CallUtil.isVideoEnabled(context)) { 1317 thirdIcon = res.getDrawable(R.drawable.ic_videocam); 1318 thirdIntent = CallUtil.getVideoCallIntent(phone.getNumber(), 1319 CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY); 1320 thirdContentDescription = 1321 res.getString(R.string.description_video_call); 1322 } 1323 } 1324 } else if (dataItem instanceof EmailDataItem) { 1325 final EmailDataItem email = (EmailDataItem) dataItem; 1326 final String address = email.getData(); 1327 if (!TextUtils.isEmpty(address)) { 1328 primaryContentDescription.append(res.getString(R.string.email_other)).append(" "); 1329 final Uri mailUri = Uri.fromParts(ContactsUtils.SCHEME_MAILTO, address, null); 1330 intent = new Intent(Intent.ACTION_SENDTO, mailUri); 1331 header = email.getAddress(); 1332 entryContextMenuInfo = new EntryContextMenuInfo(header, 1333 res.getString(R.string.emailLabelsGroup)); 1334 if (email.hasKindTypeColumn(kind)) { 1335 text = Email.getTypeLabel(res, email.getKindTypeColumn(kind), 1336 email.getLabel()).toString(); 1337 primaryContentDescription.append(text).append(" "); 1338 } 1339 primaryContentDescription.append(header); 1340 icon = res.getDrawable(R.drawable.ic_email_24dp); 1341 iconResourceId = R.drawable.ic_email_24dp; 1342 } 1343 } else if (dataItem instanceof StructuredPostalDataItem) { 1344 StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem; 1345 final String postalAddress = postal.getFormattedAddress(); 1346 if (!TextUtils.isEmpty(postalAddress)) { 1347 primaryContentDescription.append(res.getString(R.string.map_other)).append(" "); 1348 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress); 1349 header = postal.getFormattedAddress(); 1350 entryContextMenuInfo = new EntryContextMenuInfo(header, 1351 res.getString(R.string.postalLabelsGroup)); 1352 if (postal.hasKindTypeColumn(kind)) { 1353 text = StructuredPostal.getTypeLabel(res, 1354 postal.getKindTypeColumn(kind), postal.getLabel()).toString(); 1355 primaryContentDescription.append(text).append(" "); 1356 } 1357 primaryContentDescription.append(header); 1358 alternateIntent = 1359 StructuredPostalUtils.getViewPostalAddressDirectionsIntent(postalAddress); 1360 alternateIcon = res.getDrawable(R.drawable.ic_directions_24dp); 1361 alternateContentDescription.append(res.getString( 1362 R.string.content_description_directions)).append(" ").append(header); 1363 icon = res.getDrawable(R.drawable.ic_place_24dp); 1364 iconResourceId = R.drawable.ic_place_24dp; 1365 } 1366 } else if (dataItem instanceof SipAddressDataItem) { 1367 if (PhoneCapabilityTester.isSipPhone(context)) { 1368 final SipAddressDataItem sip = (SipAddressDataItem) dataItem; 1369 final String address = sip.getSipAddress(); 1370 if (!TextUtils.isEmpty(address)) { 1371 primaryContentDescription.append(res.getString(R.string.call_other)).append( 1372 " "); 1373 final Uri callUri = Uri.fromParts(PhoneAccount.SCHEME_SIP, address, null); 1374 intent = CallUtil.getCallIntent(callUri); 1375 header = address; 1376 entryContextMenuInfo = new EntryContextMenuInfo(header, 1377 res.getString(R.string.phoneLabelsGroup)); 1378 if (sip.hasKindTypeColumn(kind)) { 1379 text = SipAddress.getTypeLabel(res, 1380 sip.getKindTypeColumn(kind), sip.getLabel()).toString(); 1381 primaryContentDescription.append(text).append(" "); 1382 } 1383 primaryContentDescription.append(header); 1384 icon = res.getDrawable(R.drawable.ic_dialer_sip_black_24dp); 1385 iconResourceId = R.drawable.ic_dialer_sip_black_24dp; 1386 } 1387 } 1388 } else if (dataItem instanceof StructuredNameDataItem) { 1389 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName(); 1390 if (!TextUtils.isEmpty(givenName)) { 1391 aboutCardName.value = res.getString(R.string.about_card_title) + 1392 " " + givenName; 1393 } else { 1394 aboutCardName.value = res.getString(R.string.about_card_title); 1395 } 1396 } else { 1397 // Custom DataItem 1398 header = dataItem.buildDataStringForDisplay(context, kind); 1399 text = kind.typeColumn; 1400 intent = new Intent(Intent.ACTION_VIEW); 1401 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId()); 1402 intent.setDataAndType(uri, dataItem.getMimeType()); 1403 1404 if (intent != null) { 1405 final String mimetype = intent.getType(); 1406 1407 // Attempt to use known icons for known 3p types. Otherwise default to ResolveCache 1408 switch (mimetype) { 1409 case MIMETYPE_GPLUS_PROFILE: 1410 if (INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE.equals( 1411 intent.getDataString())) { 1412 icon = res.getDrawable( 1413 R.drawable.ic_add_to_circles_black_24); 1414 iconResourceId = R.drawable.ic_add_to_circles_black_24; 1415 } else { 1416 icon = res.getDrawable(R.drawable.ic_google_plus_24dp); 1417 iconResourceId = R.drawable.ic_google_plus_24dp; 1418 } 1419 break; 1420 case MIMETYPE_HANGOUTS: 1421 if (INTENT_DATA_HANGOUTS_VIDEO.equals(intent.getDataString())) { 1422 icon = res.getDrawable(R.drawable.ic_hangout_video_24dp); 1423 iconResourceId = R.drawable.ic_hangout_video_24dp; 1424 } else { 1425 icon = res.getDrawable(R.drawable.ic_hangout_24dp); 1426 iconResourceId = R.drawable.ic_hangout_24dp; 1427 } 1428 break; 1429 default: 1430 entryContextMenuInfo = new EntryContextMenuInfo(header, mimetype); 1431 icon = ResolveCache.getInstance(context).getIcon( 1432 dataItem.getMimeType(), intent); 1433 // Call mutate to create a new Drawable.ConstantState for color filtering 1434 if (icon != null) { 1435 icon.mutate(); 1436 } 1437 shouldApplyColor = false; 1438 } 1439 } 1440 } 1441 1442 if (intent != null) { 1443 // Do not set the intent is there are no resolves 1444 if (!PhoneCapabilityTester.isIntentRegistered(context, intent)) { 1445 intent = null; 1446 } 1447 } 1448 1449 if (alternateIntent != null) { 1450 // Do not set the alternate intent is there are no resolves 1451 if (!PhoneCapabilityTester.isIntentRegistered(context, alternateIntent)) { 1452 alternateIntent = null; 1453 } else if (TextUtils.isEmpty(alternateContentDescription)) { 1454 // Attempt to use package manager to find a suitable content description if needed 1455 alternateContentDescription.append(getIntentResolveLabel(alternateIntent, context)); 1456 } 1457 } 1458 1459 // If the Entry has no visual elements, return null 1460 if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) && 1461 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) { 1462 return null; 1463 } 1464 1465 // Ignore dataIds from the Me profile. 1466 final int dataId = dataItem.getId() > Integer.MAX_VALUE ? 1467 -1 : (int) dataItem.getId(); 1468 1469 return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon, 1470 primaryContentDescription.toString(), intent, alternateIcon, alternateIntent, 1471 alternateContentDescription.toString(), shouldApplyColor, isEditable, 1472 entryContextMenuInfo, thirdIcon, thirdIntent, thirdContentDescription, 1473 iconResourceId); 1474 } 1475 1476 private List<Entry> dataItemsToEntries(List<DataItem> dataItems, 1477 MutableString aboutCardTitleOut) { 1478 final List<Entry> entries = new ArrayList<>(); 1479 for (DataItem dataItem : dataItems) { 1480 final Entry entry = dataItemToEntry(dataItem, this, mContactData, aboutCardTitleOut); 1481 if (entry != null) { 1482 entries.add(entry); 1483 } 1484 } 1485 return entries; 1486 } 1487 1488 private static String getIntentResolveLabel(Intent intent, Context context) { 1489 final List<ResolveInfo> matches = context.getPackageManager().queryIntentActivities(intent, 1490 PackageManager.MATCH_DEFAULT_ONLY); 1491 1492 // Pick first match, otherwise best found 1493 ResolveInfo bestResolve = null; 1494 final int size = matches.size(); 1495 if (size == 1) { 1496 bestResolve = matches.get(0); 1497 } else if (size > 1) { 1498 bestResolve = ResolveCache.getInstance(context).getBestResolve(intent, matches); 1499 } 1500 1501 if (bestResolve == null) { 1502 return null; 1503 } 1504 1505 return String.valueOf(bestResolve.loadLabel(context.getPackageManager())); 1506 } 1507 1508 /** 1509 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 1510 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 1511 * on a Nexus 5. 1512 */ 1513 private void extractAndApplyTintFromPhotoViewAsynchronously() { 1514 if (mScroller == null) { 1515 return; 1516 } 1517 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1518 new AsyncTask<Void, Void, MaterialPalette>() { 1519 @Override 1520 protected MaterialPalette doInBackground(Void... params) { 1521 1522 if (imageViewDrawable instanceof BitmapDrawable 1523 && mContactData.getThumbnailPhotoBinaryData() != null 1524 && mContactData.getThumbnailPhotoBinaryData().length > 0) { 1525 // Perform the color analysis on the thumbnail instead of the full sized 1526 // image, so that our results will be as similar as possible to the Bugle 1527 // app. 1528 final Bitmap bitmap = BitmapFactory.decodeByteArray( 1529 mContactData.getThumbnailPhotoBinaryData(), 0, 1530 mContactData.getThumbnailPhotoBinaryData().length); 1531 try { 1532 final int primaryColor = colorFromBitmap(bitmap); 1533 if (primaryColor != 0) { 1534 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 1535 primaryColor); 1536 } 1537 } finally { 1538 bitmap.recycle(); 1539 } 1540 } 1541 if (imageViewDrawable instanceof LetterTileDrawable) { 1542 final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor(); 1543 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(primaryColor); 1544 } 1545 return MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(getResources()); 1546 } 1547 1548 @Override 1549 protected void onPostExecute(MaterialPalette palette) { 1550 super.onPostExecute(palette); 1551 if (mHasComputedThemeColor) { 1552 // If we had previously computed a theme color from the contact photo, 1553 // then do not update the theme color. Changing the theme color several 1554 // seconds after QC has started, as a result of an updated/upgraded photo, 1555 // is a jarring experience. On the other hand, changing the theme color after 1556 // a rotation or onNewIntent() is perfectly fine. 1557 return; 1558 } 1559 // Check that the Photo has not changed. If it has changed, the new tint 1560 // color needs to be extracted 1561 if (imageViewDrawable == mPhotoView.getDrawable()) { 1562 mHasComputedThemeColor = true; 1563 setThemeColor(palette); 1564 } 1565 } 1566 }.execute(); 1567 } 1568 1569 /** 1570 * Examine how many white pixels are in the bitmap in order to determine whether or not 1571 * we need gradient overlays on top of the image. 1572 */ 1573 private void analyzeWhitenessOfPhotoAsynchronously() { 1574 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1575 new AsyncTask<Void, Void, Boolean>() { 1576 @Override 1577 protected Boolean doInBackground(Void... params) { 1578 if (imageViewDrawable instanceof BitmapDrawable) { 1579 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 1580 return WhitenessUtils.isBitmapWhiteAtTopOrBottom(bitmap); 1581 } 1582 return !(imageViewDrawable instanceof LetterTileDrawable); 1583 } 1584 1585 @Override 1586 protected void onPostExecute(Boolean isWhite) { 1587 super.onPostExecute(isWhite); 1588 mScroller.setUseGradient(isWhite); 1589 } 1590 }.execute(); 1591 } 1592 1593 private void setThemeColor(MaterialPalette palette) { 1594 // If the color is invalid, use the predefined default 1595 final int primaryColor = palette.mPrimaryColor; 1596 mScroller.setHeaderTintColor(primaryColor); 1597 mStatusBarColor = palette.mSecondaryColor; 1598 updateStatusBarColor(); 1599 1600 mColorFilter = 1601 new PorterDuffColorFilter(primaryColor, PorterDuff.Mode.SRC_ATOP); 1602 mContactCard.setColorAndFilter(primaryColor, mColorFilter); 1603 mRecentCard.setColorAndFilter(primaryColor, mColorFilter); 1604 mAboutCard.setColorAndFilter(primaryColor, mColorFilter); 1605 } 1606 1607 private void updateStatusBarColor() { 1608 if (mScroller == null) { 1609 return; 1610 } 1611 final int desiredStatusBarColor; 1612 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 1613 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 1614 desiredStatusBarColor = mStatusBarColor; 1615 } else { 1616 desiredStatusBarColor = Color.TRANSPARENT; 1617 } 1618 // Animate to the new color. 1619 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 1620 getWindow().getStatusBarColor(), desiredStatusBarColor); 1621 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 1622 animation.setEvaluator(new ArgbEvaluator()); 1623 animation.start(); 1624 } 1625 1626 private int colorFromBitmap(Bitmap bitmap) { 1627 // Author of Palette recommends using 24 colors when analyzing profile photos. 1628 final int NUMBER_OF_PALETTE_COLORS = 24; 1629 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 1630 if (palette != null && palette.getVibrantSwatch() != null) { 1631 return palette.getVibrantSwatch().getRgb(); 1632 } 1633 return 0; 1634 } 1635 1636 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 1637 final List<Entry> entries = new ArrayList<>(); 1638 for (ContactInteraction interaction : interactions) { 1639 entries.add(new Entry(/* id = */ -1, 1640 interaction.getIcon(this), 1641 interaction.getViewHeader(this), 1642 interaction.getViewBody(this), 1643 interaction.getBodyIcon(this), 1644 interaction.getViewFooter(this), 1645 interaction.getFooterIcon(this), 1646 interaction.getContentDescription(this), 1647 interaction.getIntent(), 1648 /* alternateIcon = */ null, 1649 /* alternateIntent = */ null, 1650 /* alternateContentDescription = */ null, 1651 /* shouldApplyColor = */ true, 1652 /* isEditable = */ false, 1653 /* EntryContextMenuInfo = */ null, 1654 /* thirdIcon = */ null, 1655 /* thirdIntent = */ null, 1656 /* thirdContentDescription = */ null, 1657 interaction.getIconResourceId())); 1658 } 1659 return entries; 1660 } 1661 1662 private final LoaderCallbacks<Contact> mLoaderContactCallbacks = 1663 new LoaderCallbacks<Contact>() { 1664 @Override 1665 public void onLoaderReset(Loader<Contact> loader) { 1666 mContactData = null; 1667 } 1668 1669 @Override 1670 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1671 Trace.beginSection("onLoadFinished()"); 1672 1673 if (isFinishing()) { 1674 return; 1675 } 1676 if (data.isError()) { 1677 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 1678 // should log the actual exception. 1679 throw new IllegalStateException("Failed to load contact", data.getException()); 1680 } 1681 if (data.isNotFound()) { 1682 if (mHasAlreadyBeenOpened) { 1683 finish(); 1684 } else { 1685 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 1686 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 1687 Toast.LENGTH_LONG).show(); 1688 } 1689 return; 1690 } 1691 1692 bindContactData(data); 1693 1694 Trace.endSection(); 1695 } 1696 1697 @Override 1698 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1699 if (mLookupUri == null) { 1700 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 1701 } 1702 // Load all contact data. We need loadGroupMetaData=true to determine whether the 1703 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 1704 return new ContactLoader(getApplicationContext(), mLookupUri, 1705 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 1706 true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 1707 } 1708 }; 1709 1710 @Override 1711 public void onBackPressed() { 1712 if (mScroller != null) { 1713 if (!mIsExitAnimationInProgress) { 1714 mScroller.scrollOffBottom(); 1715 } 1716 } else { 1717 super.onBackPressed(); 1718 } 1719 } 1720 1721 @Override 1722 public void finish() { 1723 super.finish(); 1724 1725 // override transitions to skip the standard window animations 1726 overridePendingTransition(0, 0); 1727 } 1728 1729 private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 1730 new LoaderCallbacks<List<ContactInteraction>>() { 1731 1732 @Override 1733 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 1734 Log.v(TAG, "onCreateLoader"); 1735 Loader<List<ContactInteraction>> loader = null; 1736 switch (id) { 1737 case LOADER_SMS_ID: 1738 Log.v(TAG, "LOADER_SMS_ID"); 1739 loader = new SmsInteractionsLoader( 1740 QuickContactActivity.this, 1741 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1742 MAX_SMS_RETRIEVE); 1743 break; 1744 case LOADER_CALENDAR_ID: 1745 Log.v(TAG, "LOADER_CALENDAR_ID"); 1746 final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS); 1747 List<String> emailsList = null; 1748 if (emailsArray != null) { 1749 emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)); 1750 } 1751 loader = new CalendarInteractionsLoader( 1752 QuickContactActivity.this, 1753 emailsList, 1754 MAX_FUTURE_CALENDAR_RETRIEVE, 1755 MAX_PAST_CALENDAR_RETRIEVE, 1756 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 1757 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 1758 break; 1759 case LOADER_CALL_LOG_ID: 1760 Log.v(TAG, "LOADER_CALL_LOG_ID"); 1761 loader = new CallLogInteractionsLoader( 1762 QuickContactActivity.this, 1763 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1764 MAX_CALL_LOG_RETRIEVE); 1765 } 1766 return loader; 1767 } 1768 1769 @Override 1770 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 1771 List<ContactInteraction> data) { 1772 mRecentLoaderResults.put(loader.getId(), data); 1773 1774 if (isAllRecentDataLoaded()) { 1775 bindRecentData(); 1776 } 1777 } 1778 1779 @Override 1780 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 1781 mRecentLoaderResults.remove(loader.getId()); 1782 } 1783 }; 1784 1785 private boolean isAllRecentDataLoaded() { 1786 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 1787 } 1788 1789 private void bindRecentData() { 1790 final List<ContactInteraction> allInteractions = new ArrayList<>(); 1791 final List<List<Entry>> interactionsWrapper = new ArrayList<>(); 1792 1793 mRecentDataTask = new AsyncTask<Void, Void, Void>() { 1794 @Override 1795 protected Void doInBackground(Void... params) { 1796 Trace.beginSection("sort recent loader results"); 1797 1798 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 1799 allInteractions.addAll(loaderInteractions); 1800 } 1801 1802 // Sort the interactions by most recent 1803 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 1804 @Override 1805 public int compare(ContactInteraction a, ContactInteraction b) { 1806 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1; 1807 } 1808 }); 1809 1810 Trace.endSection(); 1811 Trace.beginSection("contactInteractionsToEntries"); 1812 1813 // Wrap each interaction in its own list so that an icon is displayed for each entry 1814 for (Entry contactInteraction : contactInteractionsToEntries(allInteractions)) { 1815 List<Entry> entryListWrapper = new ArrayList<>(1); 1816 entryListWrapper.add(contactInteraction); 1817 interactionsWrapper.add(entryListWrapper); 1818 } 1819 1820 Trace.endSection(); 1821 return null; 1822 } 1823 1824 @Override 1825 protected void onPostExecute(Void aVoid) { 1826 super.onPostExecute(aVoid); 1827 Trace.beginSection("initialize recents card"); 1828 1829 if (allInteractions.size() > 0) { 1830 mRecentCard.initialize(interactionsWrapper, 1831 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 1832 /* isExpanded = */ mRecentCard.isExpanded(), /* isAlwaysExpanded = */ false, 1833 mExpandingEntryCardViewListener, mScroller); 1834 mRecentCard.setVisibility(View.VISIBLE); 1835 } 1836 1837 Trace.endSection(); 1838 1839 // About card is initialized along with the contact card, but since it appears after 1840 // the recent card in the UI, we hold off until making it visible until the recent 1841 // card is also ready to avoid stuttering. 1842 if (mAboutCard.shouldShow()) { 1843 mAboutCard.setVisibility(View.VISIBLE); 1844 } else { 1845 mAboutCard.setVisibility(View.GONE); 1846 } 1847 mRecentDataTask = null; 1848 } 1849 }; 1850 mRecentDataTask.execute(); 1851 } 1852 1853 @Override 1854 protected void onStop() { 1855 super.onStop(); 1856 1857 if (mEntriesAndActionsTask != null) { 1858 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 1859 // results on the UI thread. In some circumstances Activities are killed without 1860 // onStop() being called. This is not a problem, because in these circumstances 1861 // the entire process will be killed. 1862 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 1863 } 1864 if (mRecentDataTask != null) { 1865 mRecentDataTask.cancel(/* mayInterruptIfRunning = */ false); 1866 } 1867 } 1868 1869 /** 1870 * Returns true if it is possible to edit the current contact. 1871 */ 1872 private boolean isContactEditable() { 1873 return mContactData != null && !mContactData.isDirectoryEntry(); 1874 } 1875 1876 /** 1877 * Returns true if it is possible to share the current contact. 1878 */ 1879 private boolean isContactShareable() { 1880 return mContactData != null && !mContactData.isDirectoryEntry(); 1881 } 1882 1883 private Intent getEditContactIntent() { 1884 final Intent intent = new Intent(Intent.ACTION_EDIT, mContactData.getLookupUri()); 1885 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1886 return intent; 1887 } 1888 1889 private void editContact() { 1890 mHasIntentLaunched = true; 1891 mContactLoader.cacheResult(); 1892 startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 1893 } 1894 1895 private void toggleStar(MenuItem starredMenuItem) { 1896 // Make sure there is a contact 1897 if (mContactData != null) { 1898 // Read the current starred value from the UI instead of using the last 1899 // loaded state. This allows rapid tapping without writing the same 1900 // value several times 1901 final boolean isStarred = starredMenuItem.isChecked(); 1902 1903 // To improve responsiveness, swap out the picture (and tag) in the UI already 1904 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem, 1905 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1906 !isStarred); 1907 1908 // Now perform the real save 1909 final Intent intent = ContactSaveService.createSetStarredIntent( 1910 QuickContactActivity.this, mContactData.getLookupUri(), !isStarred); 1911 startService(intent); 1912 1913 final CharSequence accessibilityText = !isStarred 1914 ? getResources().getText(R.string.description_action_menu_add_star) 1915 : getResources().getText(R.string.description_action_menu_remove_star); 1916 // Accessibility actions need to have an associated view. We can't access the MenuItem's 1917 // underlying view, so put this accessibility action on the root view. 1918 mScroller.announceForAccessibility(accessibilityText); 1919 } 1920 } 1921 1922 /** 1923 * Calls into the contacts provider to get a pre-authorized version of the given URI. 1924 */ 1925 private Uri getPreAuthorizedUri(Uri uri) { 1926 final Bundle uriBundle = new Bundle(); 1927 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri); 1928 final Bundle authResponse = getContentResolver().call( 1929 ContactsContract.AUTHORITY_URI, 1930 ContactsContract.Authorization.AUTHORIZATION_METHOD, 1931 null, 1932 uriBundle); 1933 if (authResponse != null) { 1934 return (Uri) authResponse.getParcelable( 1935 ContactsContract.Authorization.KEY_AUTHORIZED_URI); 1936 } else { 1937 return uri; 1938 } 1939 } 1940 1941 private void shareContact() { 1942 final String lookupKey = mContactData.getLookupKey(); 1943 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 1944 if (mContactData.isUserProfile()) { 1945 // User is sharing the profile. We don't want to force the receiver to have 1946 // the highly-privileged READ_PROFILE permission, so we need to request a 1947 // pre-authorized URI from the provider. 1948 shareUri = getPreAuthorizedUri(shareUri); 1949 } 1950 1951 final Intent intent = new Intent(Intent.ACTION_SEND); 1952 intent.setType(Contacts.CONTENT_VCARD_TYPE); 1953 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 1954 1955 // Launch chooser to share contact via 1956 final CharSequence chooseTitle = getText(R.string.share_via); 1957 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 1958 1959 try { 1960 mHasIntentLaunched = true; 1961 this.startActivity(chooseIntent); 1962 } catch (final ActivityNotFoundException ex) { 1963 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1964 } 1965 } 1966 1967 /** 1968 * Creates a launcher shortcut with the current contact. 1969 */ 1970 private void createLauncherShortcutWithContact() { 1971 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 1972 new OnShortcutIntentCreatedListener() { 1973 1974 @Override 1975 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 1976 // Broadcast the shortcutIntent to the launcher to create a 1977 // shortcut to this contact 1978 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 1979 QuickContactActivity.this.sendBroadcast(shortcutIntent); 1980 1981 // Send a toast to give feedback to the user that a shortcut to this 1982 // contact was added to the launcher. 1983 Toast.makeText(QuickContactActivity.this, 1984 R.string.createContactShortcutSuccessful, 1985 Toast.LENGTH_SHORT).show(); 1986 } 1987 1988 }); 1989 builder.createContactShortcutIntent(mContactData.getLookupUri()); 1990 } 1991 1992 @Override 1993 public boolean onCreateOptionsMenu(Menu menu) { 1994 final MenuInflater inflater = getMenuInflater(); 1995 inflater.inflate(R.menu.quickcontact, menu); 1996 return true; 1997 } 1998 1999 @Override 2000 public boolean onPrepareOptionsMenu(Menu menu) { 2001 if (mContactData != null) { 2002 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 2003 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem, 2004 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 2005 mContactData.getStarred()); 2006 2007 // Configure edit MenuItem 2008 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 2009 editMenuItem.setVisible(true); 2010 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 2011 .isInvisibleAndAddable(mContactData, this)) { 2012 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp); 2013 editMenuItem.setTitle(R.string.menu_add_contact); 2014 } else if (isContactEditable()) { 2015 editMenuItem.setIcon(R.drawable.ic_create_24dp); 2016 editMenuItem.setTitle(R.string.menu_editContact); 2017 } else { 2018 editMenuItem.setVisible(false); 2019 } 2020 2021 final MenuItem shareMenuItem = menu.findItem(R.id.menu_share); 2022 shareMenuItem.setVisible(isContactShareable()); 2023 2024 return true; 2025 } 2026 return false; 2027 } 2028 2029 @Override 2030 public boolean onOptionsItemSelected(MenuItem item) { 2031 switch (item.getItemId()) { 2032 case R.id.menu_star: 2033 toggleStar(item); 2034 return true; 2035 case R.id.menu_edit: 2036 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 2037 // This action is used to launch the contact selector, with the option of 2038 // creating a new contact. Creating a new contact is an INSERT, while selecting 2039 // an exisiting one is an edit. The fields in the edit screen will be 2040 // prepopulated with data. 2041 2042 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 2043 intent.setType(Contacts.CONTENT_ITEM_TYPE); 2044 2045 // Only pre-fill the name field if the provided display name is an organization 2046 // name or better (e.g. structured name, nickname) 2047 if (mContactData.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) { 2048 intent.putExtra(Intents.Insert.NAME, mContactData.getDisplayName()); 2049 } 2050 ArrayList<ContentValues> values = mContactData.getContentValues(); 2051 // Last time used and times used are aggregated values from the usage stat 2052 // table. They need to be removed from data values so the SQL table can insert 2053 // properly 2054 for (ContentValues value : values) { 2055 value.remove(Data.LAST_TIME_USED); 2056 value.remove(Data.TIMES_USED); 2057 } 2058 intent.putExtra(Intents.Insert.DATA, values); 2059 2060 // If the contact can only export to the same account, add it to the intent. 2061 // Otherwise the ContactEditorFragment will show a dialog for selecting an 2062 // account. 2063 if (mContactData.getDirectoryExportSupport() == 2064 Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY) { 2065 intent.putExtra(Intents.Insert.ACCOUNT, 2066 new Account(mContactData.getDirectoryAccountName(), 2067 mContactData.getDirectoryAccountType())); 2068 intent.putExtra(Intents.Insert.DATA_SET, 2069 mContactData.getRawContacts().get(0).getDataSet()); 2070 } 2071 2072 startActivityForResult(intent, REQUEST_CODE_CONTACT_SELECTION_ACTIVITY); 2073 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 2074 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 2075 } else if (isContactEditable()) { 2076 editContact(); 2077 } 2078 return true; 2079 case R.id.menu_share: 2080 shareContact(); 2081 return true; 2082 case R.id.menu_create_contact_shortcut: 2083 createLauncherShortcutWithContact(); 2084 return true; 2085 default: 2086 return super.onOptionsItemSelected(item); 2087 } 2088 } 2089} 2090