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