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