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