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