QuickContactActivity.java revision bcae18d136522e190d5074909e5b7148c00f0db8
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.contacts.quickcontact; 18 19import android.accounts.Account; 20import android.animation.ArgbEvaluator; 21import android.animation.ObjectAnimator; 22import android.app.Activity; 23import android.app.Fragment; 24import android.app.LoaderManager.LoaderCallbacks; 25import android.app.SearchManager; 26import android.content.ActivityNotFoundException; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.Intent; 31import android.content.Loader; 32import android.content.pm.PackageManager; 33import android.content.pm.ResolveInfo; 34import android.content.res.ColorStateList; 35import android.content.res.Resources; 36import android.graphics.Bitmap; 37import android.graphics.BitmapFactory; 38import android.graphics.Color; 39import android.graphics.PorterDuff; 40import android.graphics.PorterDuffColorFilter; 41import android.graphics.drawable.BitmapDrawable; 42import android.graphics.drawable.ColorDrawable; 43import android.graphics.drawable.Drawable; 44import android.net.Uri; 45import android.os.AsyncTask; 46import android.os.Bundle; 47import android.os.Trace; 48import android.provider.CalendarContract; 49import android.provider.ContactsContract; 50import android.provider.ContactsContract.CommonDataKinds.Email; 51import android.provider.ContactsContract.CommonDataKinds.Event; 52import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 53import android.provider.ContactsContract.CommonDataKinds.Identity; 54import android.provider.ContactsContract.CommonDataKinds.Im; 55import android.provider.ContactsContract.CommonDataKinds.Nickname; 56import android.provider.ContactsContract.CommonDataKinds.Note; 57import android.provider.ContactsContract.CommonDataKinds.Organization; 58import android.provider.ContactsContract.CommonDataKinds.Phone; 59import android.provider.ContactsContract.CommonDataKinds.Relation; 60import android.provider.ContactsContract.CommonDataKinds.SipAddress; 61import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 62import android.provider.ContactsContract.CommonDataKinds.Website; 63import android.provider.ContactsContract.Contacts; 64import android.provider.ContactsContract.Data; 65import android.provider.ContactsContract.Directory; 66import android.provider.ContactsContract.DisplayNameSources; 67import android.provider.ContactsContract.DataUsageFeedback; 68import android.provider.ContactsContract.Intents; 69import android.provider.ContactsContract.QuickContact; 70import android.provider.ContactsContract.RawContacts; 71import android.support.v4.content.ContextCompat; 72import android.support.v7.graphics.Palette; 73import android.support.v7.widget.CardView; 74import android.telecom.PhoneAccount; 75import android.telecom.TelecomManager; 76import android.text.BidiFormatter; 77import android.text.Spannable; 78import android.text.SpannableString; 79import android.text.TextDirectionHeuristics; 80import android.text.TextUtils; 81import android.util.Log; 82import android.view.ContextMenu; 83import android.view.ContextMenu.ContextMenuInfo; 84import android.view.LayoutInflater; 85import android.view.Menu; 86import android.view.MenuInflater; 87import android.view.MenuItem; 88import android.view.MotionEvent; 89import android.view.View; 90import android.view.View.OnClickListener; 91import android.view.View.OnCreateContextMenuListener; 92import android.view.WindowManager; 93import android.view.accessibility.AccessibilityEvent; 94import android.widget.Button; 95import android.widget.CheckBox; 96import android.widget.ImageView; 97import android.widget.LinearLayout; 98import android.widget.TextView; 99import android.widget.Toast; 100import android.widget.Toolbar; 101 102import com.android.contacts.ContactSaveService; 103import com.android.contacts.ContactsActivity; 104import com.android.contacts.NfcHandler; 105import com.android.contacts.R; 106import com.android.contacts.activities.ContactEditorBaseActivity; 107import com.android.contacts.common.CallUtil; 108import com.android.contacts.common.ClipboardUtils; 109import com.android.contacts.common.Collapser; 110import com.android.contacts.common.ContactPhotoManager; 111import com.android.contacts.common.ContactsUtils; 112import com.android.contacts.common.activity.RequestDesiredPermissionsActivity; 113import com.android.contacts.common.activity.RequestPermissionsActivity; 114import com.android.contacts.common.compat.CompatUtils; 115import com.android.contacts.common.compat.EventCompat; 116import com.android.contacts.common.dialog.CallSubjectDialog; 117import com.android.contacts.common.editor.SelectAccountDialogFragment; 118import com.android.contacts.common.interactions.TouchPointManager; 119import com.android.contacts.common.lettertiles.LetterTileDrawable; 120import com.android.contacts.common.list.ShortcutIntentBuilder; 121import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 122import com.android.contacts.common.logging.Logger; 123import com.android.contacts.common.logging.ScreenEvent.ScreenType; 124import com.android.contacts.common.model.AccountTypeManager; 125import com.android.contacts.common.model.Contact; 126import com.android.contacts.common.model.ContactLoader; 127import com.android.contacts.common.model.RawContact; 128import com.android.contacts.common.model.account.AccountType; 129import com.android.contacts.common.model.account.AccountWithDataSet; 130import com.android.contacts.common.model.dataitem.DataItem; 131import com.android.contacts.common.model.dataitem.DataKind; 132import com.android.contacts.common.model.dataitem.EmailDataItem; 133import com.android.contacts.common.model.dataitem.EventDataItem; 134import com.android.contacts.common.model.dataitem.ImDataItem; 135import com.android.contacts.common.model.dataitem.NicknameDataItem; 136import com.android.contacts.common.model.dataitem.NoteDataItem; 137import com.android.contacts.common.model.dataitem.OrganizationDataItem; 138import com.android.contacts.common.model.dataitem.PhoneDataItem; 139import com.android.contacts.common.model.dataitem.RelationDataItem; 140import com.android.contacts.common.model.dataitem.SipAddressDataItem; 141import com.android.contacts.common.model.dataitem.StructuredNameDataItem; 142import com.android.contacts.common.model.dataitem.StructuredPostalDataItem; 143import com.android.contacts.common.model.dataitem.WebsiteDataItem; 144import com.android.contacts.common.model.ValuesDelta; 145import com.android.contacts.common.util.ImplicitIntentsUtil; 146import com.android.contacts.common.util.DateUtils; 147import com.android.contacts.common.util.MaterialColorMapUtils; 148import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 149import com.android.contacts.common.util.UriUtils; 150import com.android.contacts.common.util.ViewUtil; 151import com.android.contacts.detail.ContactDisplayUtils; 152import com.android.contacts.editor.AggregationSuggestionEngine; 153import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; 154import com.android.contacts.editor.ContactEditorFragment; 155import com.android.contacts.editor.EditorIntents; 156import com.android.contacts.interactions.CalendarInteractionsLoader; 157import com.android.contacts.interactions.CallLogInteractionsLoader; 158import com.android.contacts.interactions.ContactDeletionInteraction; 159import com.android.contacts.interactions.ContactInteraction; 160import com.android.contacts.interactions.JoinContactsDialogFragment; 161import com.android.contacts.interactions.JoinContactsDialogFragment.JoinContactsListener; 162import com.android.contacts.interactions.SmsInteractionsLoader; 163import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 164import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryContextMenuInfo; 165import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryTag; 166import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener; 167import com.android.contacts.quickcontact.WebAddress.ParseException; 168import com.android.contacts.util.ImageViewDrawableSetter; 169import com.android.contacts.util.PhoneCapabilityTester; 170import com.android.contacts.util.SchedulingUtils; 171import com.android.contacts.util.StructuredPostalUtils; 172import com.android.contacts.widget.MultiShrinkScroller; 173import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 174import com.android.contacts.widget.QuickContactImageView; 175import com.android.contactsbind.HelpUtils; 176 177import com.google.common.collect.Lists; 178 179import java.lang.SecurityException; 180import java.util.ArrayList; 181import java.util.Arrays; 182import java.util.Calendar; 183import java.util.Collections; 184import java.util.Comparator; 185import java.util.Date; 186import java.util.HashMap; 187import java.util.HashSet; 188import java.util.List; 189import java.util.Map; 190import java.util.Set; 191import java.util.TreeSet; 192import java.util.concurrent.ConcurrentHashMap; 193 194/** 195 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 196 * data asynchronously, and then shows a popup with details centered around 197 * {@link Intent#getSourceBounds()}. 198 */ 199public class QuickContactActivity extends ContactsActivity 200 implements AggregationSuggestionEngine.Listener, JoinContactsListener { 201 202 /** 203 * QuickContacts immediately takes up the full screen. All possible information is shown. 204 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 205 * should only be used by the Contacts app. 206 */ 207 public static final int MODE_FULLY_EXPANDED = 4; 208 209 /** Used to pass the screen where the user came before launching this Activity. */ 210 public static final String EXTRA_PREVIOUS_SCREEN_TYPE = "previous_screen_type"; 211 212 private static final String TAG = "QuickContact"; 213 214 private static final String KEY_THEME_COLOR = "theme_color"; 215 private static final String KEY_IS_SUGGESTION_LIST_COLLAPSED = "is_suggestion_list_collapsed"; 216 private static final String KEY_SELECTED_SUGGESTION_CONTACTS = "selected_suggestion_contacts"; 217 private static final String KEY_PREVIOUS_CONTACT_ID = "previous_contact_id"; 218 private static final String KEY_SUGGESTIONS_AUTO_SELECTED = "suggestions_auto_seleted"; 219 220 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150; 221 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 222 private static final int SCRIM_COLOR = Color.argb(0xC8, 0, 0, 0); 223 private static final int REQUEST_CODE_CONTACT_SELECTION_ACTIVITY = 2; 224 private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms"; 225 226 /** This is the Intent action to install a shortcut in the launcher. */ 227 private static final String ACTION_INSTALL_SHORTCUT = 228 "com.android.launcher.action.INSTALL_SHORTCUT"; 229 230 @SuppressWarnings("deprecation") 231 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 232 233 private static final String MIMETYPE_GPLUS_PROFILE = 234 "vnd.android.cursor.item/vnd.googleplus.profile"; 235 private static final String GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE = "addtocircle"; 236 private static final String GPLUS_PROFILE_DATA_5_VIEW_PROFILE = "view"; 237 private static final String MIMETYPE_HANGOUTS = 238 "vnd.android.cursor.item/vnd.googleplus.profile.comm"; 239 private static final String HANGOUTS_DATA_5_VIDEO = "hangout"; 240 private static final String HANGOUTS_DATA_5_MESSAGE = "conversation"; 241 private static final String CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY = 242 "com.android.contacts.quickcontact.QuickContactActivity"; 243 244 /** 245 * The URI used to load the the Contact. Once the contact is loaded, use Contact#getLookupUri() 246 * instead of referencing this URI. 247 */ 248 private Uri mLookupUri; 249 private String[] mExcludeMimes; 250 private int mExtraMode; 251 private String mExtraPrioritizedMimeType; 252 private int mStatusBarColor; 253 private boolean mHasAlreadyBeenOpened; 254 private boolean mOnlyOnePhoneNumber; 255 private boolean mOnlyOneEmail; 256 257 private QuickContactImageView mPhotoView; 258 private ExpandingEntryCardView mContactCard; 259 private ExpandingEntryCardView mNoContactDetailsCard; 260 private ExpandingEntryCardView mRecentCard; 261 private ExpandingEntryCardView mAboutCard; 262 263 // Suggestion card. 264 private CardView mCollapsedSuggestionCardView; 265 private CardView mExpandSuggestionCardView; 266 private View mCollapasedSuggestionHeader; 267 private TextView mCollapsedSuggestionCardTitle; 268 private TextView mExpandSuggestionCardTitle; 269 private ImageView mSuggestionSummaryPhoto; 270 private TextView mSuggestionForName; 271 private TextView mSuggestionContactsNumber; 272 private LinearLayout mSuggestionList; 273 private Button mSuggestionsCancelButton; 274 private Button mSuggestionsLinkButton; 275 private boolean mIsSuggestionListCollapsed; 276 private boolean mSuggestionsShouldAutoSelected = true; 277 private long mPreviousContactId = 0; 278 279 private MultiShrinkScroller mScroller; 280 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 281 private AsyncTask<Void, Void, Cp2DataCardModel> mEntriesAndActionsTask; 282 private AsyncTask<Void, Void, Void> mRecentDataTask; 283 284 private AggregationSuggestionEngine mAggregationSuggestionEngine; 285 private List<Suggestion> mSuggestions; 286 287 private TreeSet<Long> mSelectedAggregationIds = new TreeSet<>(); 288 /** 289 * The last copy of Cp2DataCardModel that was passed to {@link #populateContactAndAboutCard}. 290 */ 291 private Cp2DataCardModel mCachedCp2DataCardModel; 292 /** 293 * This scrim's opacity is controlled in two different ways. 1) Before the initial entrance 294 * animation finishes, the opacity is animated by a value animator. This is designed to 295 * distract the user from the length of the initial loading time. 2) After the initial 296 * entrance animation, the opacity is directly related to scroll position. 297 */ 298 private ColorDrawable mWindowScrim; 299 private boolean mIsEntranceAnimationFinished; 300 private MaterialColorMapUtils mMaterialColorMapUtils; 301 private boolean mIsExitAnimationInProgress; 302 private boolean mHasComputedThemeColor; 303 304 /** 305 * Used to stop the ExpandingEntry cards from adjusting between an entry click and the intent 306 * being launched. 307 */ 308 private boolean mHasIntentLaunched; 309 310 private Contact mContactData; 311 private ContactLoader mContactLoader; 312 private PorterDuffColorFilter mColorFilter; 313 private int mColorFilterColor; 314 315 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 316 317 /** 318 * {@link #LEADING_MIMETYPES} is used to sort MIME-types. 319 * 320 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 321 * in the order specified here.</p> 322 */ 323 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 324 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, 325 StructuredPostal.CONTENT_ITEM_TYPE); 326 327 private static final List<String> SORTED_ABOUT_CARD_MIMETYPES = Lists.newArrayList( 328 Nickname.CONTENT_ITEM_TYPE, 329 // Phonetic name is inserted after nickname if it is available. 330 // No mimetype for phonetic name exists. 331 Website.CONTENT_ITEM_TYPE, 332 Organization.CONTENT_ITEM_TYPE, 333 Event.CONTENT_ITEM_TYPE, 334 Relation.CONTENT_ITEM_TYPE, 335 Im.CONTENT_ITEM_TYPE, 336 GroupMembership.CONTENT_ITEM_TYPE, 337 Identity.CONTENT_ITEM_TYPE, 338 Note.CONTENT_ITEM_TYPE); 339 340 private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance(); 341 342 /** Id for the background contact loader */ 343 private static final int LOADER_CONTACT_ID = 0; 344 345 private static final String KEY_LOADER_EXTRA_PHONES = 346 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES"; 347 348 /** Id for the background Sms Loader */ 349 private static final int LOADER_SMS_ID = 1; 350 private static final int MAX_SMS_RETRIEVE = 3; 351 352 /** Id for the back Calendar Loader */ 353 private static final int LOADER_CALENDAR_ID = 2; 354 private static final String KEY_LOADER_EXTRA_EMAILS = 355 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS"; 356 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 357 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 358 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 359 1L * 24L * 60L * 60L * 1000L /* 1 day */; 360 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 361 7L * 24L * 60L * 60L * 1000L /* 7 days */; 362 363 /** Id for the background Call Log Loader */ 364 private static final int LOADER_CALL_LOG_ID = 3; 365 private static final int MAX_CALL_LOG_RETRIEVE = 3; 366 private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3; 367 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 368 private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2; 369 370 371 private static final int[] mRecentLoaderIds = new int[]{ 372 LOADER_SMS_ID, 373 LOADER_CALENDAR_ID, 374 LOADER_CALL_LOG_ID}; 375 /** 376 * ConcurrentHashMap constructor params: 4 is initial table size, 0.9f is 377 * load factor before resizing, 1 means we only expect a single thread to 378 * write to the map so make only a single shard 379 */ 380 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults = 381 new ConcurrentHashMap<>(4, 0.9f, 1); 382 383 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 384 385 final OnClickListener mEntryClickHandler = new OnClickListener() { 386 @Override 387 public void onClick(View v) { 388 final Object entryTagObject = v.getTag(); 389 if (entryTagObject == null || !(entryTagObject instanceof EntryTag)) { 390 Log.w(TAG, "EntryTag was not used correctly"); 391 return; 392 } 393 final EntryTag entryTag = (EntryTag) entryTagObject; 394 final Intent intent = entryTag.getIntent(); 395 final int dataId = entryTag.getId(); 396 397 if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) { 398 editContact(); 399 return; 400 } 401 402 // Pass the touch point through the intent for use in the InCallUI 403 if (Intent.ACTION_CALL.equals(intent.getAction())) { 404 if (TouchPointManager.getInstance().hasValidPoint()) { 405 Bundle extras = new Bundle(); 406 extras.putParcelable(TouchPointManager.TOUCH_POINT, 407 TouchPointManager.getInstance().getPoint()); 408 intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras); 409 } 410 } 411 412 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 413 414 mHasIntentLaunched = true; 415 try { 416 ImplicitIntentsUtil.startActivityInAppIfPossible(QuickContactActivity.this, intent); 417 } catch (SecurityException ex) { 418 Toast.makeText(QuickContactActivity.this, R.string.missing_app, 419 Toast.LENGTH_SHORT).show(); 420 Log.e(TAG, "QuickContacts does not have permission to launch " 421 + intent); 422 } catch (ActivityNotFoundException ex) { 423 Toast.makeText(QuickContactActivity.this, R.string.missing_app, 424 Toast.LENGTH_SHORT).show(); 425 } 426 427 // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id 428 // so the exact usage type is not necessary in all cases 429 String usageType = DataUsageFeedback.USAGE_TYPE_CALL; 430 431 final Uri intentUri = intent.getData(); 432 if ((intentUri != null && intentUri.getScheme() != null && 433 intentUri.getScheme().equals(ContactsUtils.SCHEME_SMSTO)) || 434 (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) { 435 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT; 436 } 437 438 // Data IDs start at 1 so anything less is invalid 439 if (dataId > 0) { 440 final Uri dataUsageUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() 441 .appendPath(String.valueOf(dataId)) 442 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType) 443 .build(); 444 try { 445 final boolean successful = getContentResolver().update( 446 dataUsageUri, new ContentValues(), null, null) > 0; 447 if (!successful) { 448 Log.w(TAG, "DataUsageFeedback increment failed"); 449 } 450 } catch (SecurityException ex) { 451 Log.w(TAG, "DataUsageFeedback increment failed", ex); 452 } 453 } else { 454 Log.w(TAG, "Invalid Data ID"); 455 } 456 } 457 }; 458 459 final ExpandingEntryCardViewListener mExpandingEntryCardViewListener 460 = new ExpandingEntryCardViewListener() { 461 @Override 462 public void onCollapse(int heightDelta) { 463 mScroller.prepareForShrinkingScrollChild(heightDelta); 464 } 465 466 @Override 467 public void onExpand() { 468 mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ true); 469 } 470 471 @Override 472 public void onExpandDone() { 473 mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ false); 474 } 475 }; 476 477 @Override 478 public void onAggregationSuggestionChange() { 479 if (mAggregationSuggestionEngine == null) { 480 return; 481 } 482 mSuggestions = mAggregationSuggestionEngine.getSuggestions(); 483 mCollapsedSuggestionCardView.setVisibility(View.GONE); 484 mExpandSuggestionCardView.setVisibility(View.GONE); 485 mSuggestionList.removeAllViews(); 486 487 if (mContactData == null) { 488 return; 489 } 490 491 final String suggestionForName = mContactData.getDisplayName(); 492 final int suggestionNumber = mSuggestions.size(); 493 494 if (suggestionNumber <= 0) { 495 mSelectedAggregationIds.clear(); 496 return; 497 } 498 499 ContactPhotoManager.DefaultImageRequest 500 request = new ContactPhotoManager.DefaultImageRequest( 501 suggestionForName, mContactData.getLookupKey(), ContactPhotoManager.TYPE_DEFAULT, 502 /* isCircular */ true ); 503 final long photoId = mContactData.getPhotoId(); 504 final byte[] photoBytes = mContactData.getThumbnailPhotoBinaryData(); 505 if (photoBytes != null) { 506 ContactPhotoManager.getInstance(this).loadThumbnail(mSuggestionSummaryPhoto, photoId, 507 /* darkTheme */ false , /* isCircular */ true , request); 508 } else { 509 ContactPhotoManager.DEFAULT_AVATAR.applyDefaultImage(mSuggestionSummaryPhoto, 510 -1, false, request); 511 } 512 513 final String suggestionTitle = getResources().getQuantityString( 514 R.plurals.quickcontact_suggestion_card_title, suggestionNumber, suggestionNumber); 515 mCollapsedSuggestionCardTitle.setText(suggestionTitle); 516 mExpandSuggestionCardTitle.setText(suggestionTitle); 517 518 mSuggestionForName.setText(suggestionForName); 519 final int linkedContactsNumber = mContactData.getRawContacts().size(); 520 final String contactsInfo; 521 final String accountName = mContactData.getRawContacts().get(0).getAccountName(); 522 if (linkedContactsNumber == 1 && accountName == null) { 523 mSuggestionContactsNumber.setVisibility(View.INVISIBLE); 524 } 525 if (linkedContactsNumber == 1 && accountName != null) { 526 contactsInfo = getResources().getString(R.string.contact_from_account_name, 527 accountName); 528 } else { 529 contactsInfo = getResources().getString( 530 R.string.quickcontact_contacts_number, linkedContactsNumber); 531 } 532 mSuggestionContactsNumber.setText(contactsInfo); 533 534 final Set<Long> suggestionContactIds = new HashSet<>(); 535 for (Suggestion suggestion : mSuggestions) { 536 mSuggestionList.addView(inflateSuggestionListView(suggestion)); 537 suggestionContactIds.add(suggestion.contactId); 538 } 539 540 if (mIsSuggestionListCollapsed) { 541 collapseSuggestionList(); 542 } else { 543 expandSuggestionList(); 544 } 545 546 // Remove contact Ids that are not suggestions. 547 final Set<Long> selectedSuggestionIds = com.google.common.collect.Sets.intersection( 548 mSelectedAggregationIds, suggestionContactIds); 549 mSelectedAggregationIds = new TreeSet<>(selectedSuggestionIds); 550 if (!mSelectedAggregationIds.isEmpty()) { 551 enableLinkButton(); 552 } 553 } 554 555 private void collapseSuggestionList() { 556 mCollapsedSuggestionCardView.setVisibility(View.VISIBLE); 557 mExpandSuggestionCardView.setVisibility(View.GONE); 558 mIsSuggestionListCollapsed = true; 559 } 560 561 private void expandSuggestionList() { 562 mCollapsedSuggestionCardView.setVisibility(View.GONE); 563 mExpandSuggestionCardView.setVisibility(View.VISIBLE); 564 mIsSuggestionListCollapsed = false; 565 } 566 567 private View inflateSuggestionListView(final Suggestion suggestion) { 568 final LayoutInflater layoutInflater = LayoutInflater.from(this); 569 final View suggestionView = layoutInflater.inflate( 570 R.layout.quickcontact_suggestion_contact_item, null); 571 572 ContactPhotoManager.DefaultImageRequest 573 request = new ContactPhotoManager.DefaultImageRequest( 574 suggestion.name, suggestion.lookupKey, ContactPhotoManager.TYPE_DEFAULT, /* 575 isCircular */ true); 576 final ImageView photo = (ImageView) suggestionView.findViewById( 577 R.id.aggregation_suggestion_photo); 578 if (suggestion.photo != null) { 579 ContactPhotoManager.getInstance(this).loadThumbnail(photo, suggestion.photoId, 580 /* darkTheme */ false, /* isCircular */ true, request); 581 } else { 582 ContactPhotoManager.DEFAULT_AVATAR.applyDefaultImage(photo, -1, false, request); 583 } 584 585 final TextView name = (TextView) suggestionView.findViewById(R.id.aggregation_suggestion_name); 586 name.setText(suggestion.name); 587 588 final TextView accountNameView = (TextView) suggestionView.findViewById( 589 R.id.aggregation_suggestion_account_name); 590 final String accountName = suggestion.rawContacts.get(0).accountName; 591 if (!TextUtils.isEmpty(accountName)) { 592 accountNameView.setText( 593 getResources().getString(R.string.contact_from_account_name, accountName)); 594 } else { 595 accountNameView.setVisibility(View.INVISIBLE); 596 } 597 598 final CheckBox checkbox = (CheckBox) suggestionView.findViewById(R.id.suggestion_checkbox); 599 final int[][] stateSet = new int[][] { 600 new int[] { android.R.attr.state_checked }, 601 new int[] { -android.R.attr.state_checked } 602 }; 603 final int[] colors = new int[] { mColorFilterColor, mColorFilterColor }; 604 if (suggestion != null && suggestion.name != null) { 605 checkbox.setContentDescription(suggestion.name + " " + 606 getResources().getString(R.string.contact_from_account_name, accountName)); 607 } 608 checkbox.setButtonTintList(new ColorStateList(stateSet, colors)); 609 checkbox.setChecked(mSuggestionsShouldAutoSelected || 610 mSelectedAggregationIds.contains(suggestion.contactId)); 611 if (checkbox.isChecked()) { 612 mSelectedAggregationIds.add(suggestion.contactId); 613 } 614 checkbox.setTag(suggestion.contactId); 615 checkbox.setOnClickListener(new OnClickListener() { 616 @Override 617 public void onClick(View v) { 618 final CheckBox checkBox = (CheckBox) v; 619 final Long contactId = (Long) checkBox.getTag(); 620 if (mSelectedAggregationIds.contains(mContactData.getId())) { 621 mSelectedAggregationIds.remove(mContactData.getId()); 622 } 623 if (checkBox.isChecked()) { 624 mSelectedAggregationIds.add(contactId); 625 if (mSelectedAggregationIds.size() >= 1) { 626 enableLinkButton(); 627 } 628 } else { 629 mSelectedAggregationIds.remove(contactId); 630 mSuggestionsShouldAutoSelected = false; 631 if (mSelectedAggregationIds.isEmpty()) { 632 disableLinkButton(); 633 } 634 } 635 } 636 }); 637 638 return suggestionView; 639 } 640 641 private void enableLinkButton() { 642 mSuggestionsLinkButton.setClickable(true); 643 mSuggestionsLinkButton.getBackground().setColorFilter(mColorFilter); 644 mSuggestionsLinkButton.setTextColor( 645 ContextCompat.getColor(this, android.R.color.white)); 646 mSuggestionsLinkButton.setOnClickListener(new OnClickListener() { 647 @Override 648 public void onClick(View view) { 649 // Join selected contacts. 650 if (!mSelectedAggregationIds.contains(mContactData.getId())) { 651 mSelectedAggregationIds.add(mContactData.getId()); 652 } 653 JoinContactsDialogFragment.start( 654 QuickContactActivity.this, mSelectedAggregationIds); 655 } 656 }); 657 } 658 659 @Override 660 public void onContactsJoined() { 661 disableLinkButton(); 662 } 663 664 private void disableLinkButton() { 665 mSuggestionsLinkButton.setClickable(false); 666 mSuggestionsLinkButton.getBackground().setColorFilter( 667 ContextCompat.getColor(this, R.color.disabled_button_background), 668 PorterDuff.Mode.SRC_ATOP); 669 mSuggestionsLinkButton.setTextColor( 670 ContextCompat.getColor(this, R.color.disabled_button_text)); 671 } 672 673 private interface ContextMenuIds { 674 static final int COPY_TEXT = 0; 675 static final int CLEAR_DEFAULT = 1; 676 static final int SET_DEFAULT = 2; 677 } 678 679 private final OnCreateContextMenuListener mEntryContextMenuListener = 680 new OnCreateContextMenuListener() { 681 @Override 682 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 683 if (menuInfo == null) { 684 return; 685 } 686 final EntryContextMenuInfo info = (EntryContextMenuInfo) menuInfo; 687 menu.setHeaderTitle(info.getCopyText()); 688 menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT, 689 ContextMenu.NONE, getString(R.string.copy_text)); 690 691 // Don't allow setting or clearing of defaults for non-editable contacts 692 if (!isContactEditable()) { 693 return; 694 } 695 696 final String selectedMimeType = info.getMimeType(); 697 698 // Defaults to true will only enable the detail to be copied to the clipboard. 699 boolean onlyOneOfMimeType = true; 700 701 // Only allow primary support for Phone and Email content types 702 if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 703 onlyOneOfMimeType = mOnlyOnePhoneNumber; 704 } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 705 onlyOneOfMimeType = mOnlyOneEmail; 706 } 707 708 // Checking for previously set default 709 if (info.isSuperPrimary()) { 710 menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT, 711 ContextMenu.NONE, getString(R.string.clear_default)); 712 } else if (!onlyOneOfMimeType) { 713 menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT, 714 ContextMenu.NONE, getString(R.string.set_default)); 715 } 716 } 717 }; 718 719 @Override 720 public boolean onContextItemSelected(MenuItem item) { 721 EntryContextMenuInfo menuInfo; 722 try { 723 menuInfo = (EntryContextMenuInfo) item.getMenuInfo(); 724 } catch (ClassCastException e) { 725 Log.e(TAG, "bad menuInfo", e); 726 return false; 727 } 728 729 switch (item.getItemId()) { 730 case ContextMenuIds.COPY_TEXT: 731 ClipboardUtils.copyText(this, menuInfo.getCopyLabel(), menuInfo.getCopyText(), 732 true); 733 return true; 734 case ContextMenuIds.SET_DEFAULT: 735 final Intent setIntent = ContactSaveService.createSetSuperPrimaryIntent(this, 736 menuInfo.getId()); 737 this.startService(setIntent); 738 return true; 739 case ContextMenuIds.CLEAR_DEFAULT: 740 final Intent clearIntent = ContactSaveService.createClearPrimaryIntent(this, 741 menuInfo.getId()); 742 this.startService(clearIntent); 743 return true; 744 default: 745 throw new IllegalArgumentException("Unknown menu option " + item.getItemId()); 746 } 747 } 748 749 /** 750 * Headless fragment used to handle account selection callbacks invoked from 751 * {@link DirectoryContactUtil}. 752 */ 753 public static class SelectAccountDialogFragmentListener extends Fragment 754 implements SelectAccountDialogFragment.Listener { 755 756 private QuickContactActivity mQuickContactActivity; 757 758 public SelectAccountDialogFragmentListener() {} 759 760 @Override 761 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 762 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 763 account, mQuickContactActivity); 764 } 765 766 @Override 767 public void onAccountSelectorCancelled() {} 768 769 /** 770 * Set the parent activity. Since rotation can cause this fragment to be used across 771 * more than one activity instance, we need to explicitly set this value instead 772 * of making this class non-static. 773 */ 774 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 775 mQuickContactActivity = quickContactActivity; 776 } 777 } 778 779 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 780 = new MultiShrinkScrollerListener() { 781 @Override 782 public void onScrolledOffBottom() { 783 finish(); 784 } 785 786 @Override 787 public void onEnterFullscreen() { 788 updateStatusBarColor(); 789 } 790 791 @Override 792 public void onExitFullscreen() { 793 updateStatusBarColor(); 794 } 795 796 @Override 797 public void onStartScrollOffBottom() { 798 mIsExitAnimationInProgress = true; 799 } 800 801 @Override 802 public void onEntranceAnimationDone() { 803 mIsEntranceAnimationFinished = true; 804 } 805 806 @Override 807 public void onTransparentViewHeightChange(float ratio) { 808 if (mIsEntranceAnimationFinished) { 809 mWindowScrim.setAlpha((int) (0xFF * ratio)); 810 } 811 } 812 }; 813 814 815 /** 816 * Data items are compared to the same mimetype based off of three qualities: 817 * 1. Super primary 818 * 2. Primary 819 * 3. Times used 820 */ 821 private final Comparator<DataItem> mWithinMimeTypeDataItemComparator = 822 new Comparator<DataItem>() { 823 @Override 824 public int compare(DataItem lhs, DataItem rhs) { 825 if (!lhs.getMimeType().equals(rhs.getMimeType())) { 826 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " + 827 lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType()); 828 return 0; 829 } 830 831 if (lhs.isSuperPrimary()) { 832 return -1; 833 } else if (rhs.isSuperPrimary()) { 834 return 1; 835 } else if (lhs.isPrimary() && !rhs.isPrimary()) { 836 return -1; 837 } else if (!lhs.isPrimary() && rhs.isPrimary()) { 838 return 1; 839 } else { 840 final int lhsTimesUsed = 841 lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 842 final int rhsTimesUsed = 843 rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 844 845 return rhsTimesUsed - lhsTimesUsed; 846 } 847 } 848 }; 849 850 /** 851 * Sorts among different mimetypes based off: 852 * 1. Whether one of the mimetypes is the prioritized mimetype 853 * 2. Number of times used 854 * 3. Last time used 855 * 4. Statically defined 856 */ 857 private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator = 858 new Comparator<List<DataItem>> () { 859 @Override 860 public int compare(List<DataItem> lhsList, List<DataItem> rhsList) { 861 final DataItem lhs = lhsList.get(0); 862 final DataItem rhs = rhsList.get(0); 863 final String lhsMimeType = lhs.getMimeType(); 864 final String rhsMimeType = rhs.getMimeType(); 865 866 // 1. Whether one of the mimetypes is the prioritized mimetype 867 if (!TextUtils.isEmpty(mExtraPrioritizedMimeType) && !lhsMimeType.equals(rhsMimeType)) { 868 if (rhsMimeType.equals(mExtraPrioritizedMimeType)) { 869 return 1; 870 } 871 if (lhsMimeType.equals(mExtraPrioritizedMimeType)) { 872 return -1; 873 } 874 } 875 876 // 2. Number of times used 877 final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 878 final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 879 final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed; 880 if (timesUsedDifference != 0) { 881 return timesUsedDifference; 882 } 883 884 // 3. Last time used 885 final long lhsLastTimeUsed = 886 lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed(); 887 final long rhsLastTimeUsed = 888 rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed(); 889 final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed; 890 if (lastTimeUsedDifference > 0) { 891 return 1; 892 } else if (lastTimeUsedDifference < 0) { 893 return -1; 894 } 895 896 // 4. Resort to a statically defined mimetype order. 897 if (!lhsMimeType.equals(rhsMimeType)) { 898 for (String mimeType : LEADING_MIMETYPES) { 899 if (lhsMimeType.equals(mimeType)) { 900 return -1; 901 } else if (rhsMimeType.equals(mimeType)) { 902 return 1; 903 } 904 } 905 } 906 return 0; 907 } 908 }; 909 910 @Override 911 public boolean dispatchTouchEvent(MotionEvent ev) { 912 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 913 TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); 914 } 915 return super.dispatchTouchEvent(ev); 916 } 917 918 @Override 919 protected void onCreate(Bundle savedInstanceState) { 920 Trace.beginSection("onCreate()"); 921 super.onCreate(savedInstanceState); 922 923 if (RequestPermissionsActivity.startPermissionActivity(this) || 924 RequestDesiredPermissionsActivity.startPermissionActivity(this)) { 925 return; 926 } 927 928 final int previousScreenType = getIntent().getExtras() 929 .getInt(EXTRA_PREVIOUS_SCREEN_TYPE, ScreenType.UNKNOWN); 930 Logger.logScreenView(this, ScreenType.QUICK_CONTACT, previousScreenType); 931 932 if (CompatUtils.isLollipopCompatible()) { 933 getWindow().setStatusBarColor(Color.TRANSPARENT); 934 } 935 936 processIntent(getIntent()); 937 938 // Show QuickContact in front of soft input 939 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 940 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 941 942 setContentView(R.layout.quickcontact_activity); 943 944 mMaterialColorMapUtils = new MaterialColorMapUtils(getResources()); 945 946 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 947 948 mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 949 mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card); 950 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 951 mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card); 952 953 mCollapsedSuggestionCardView = (CardView) findViewById(R.id.collapsed_suggestion_card); 954 mExpandSuggestionCardView = (CardView) findViewById(R.id.expand_suggestion_card); 955 mCollapasedSuggestionHeader = findViewById(R.id.collapsed_suggestion_header); 956 mCollapsedSuggestionCardTitle = (TextView) findViewById( 957 R.id.collapsed_suggestion_card_title); 958 mExpandSuggestionCardTitle = (TextView) findViewById(R.id.expand_suggestion_card_title); 959 mSuggestionSummaryPhoto = (ImageView) findViewById(R.id.suggestion_icon); 960 mSuggestionForName = (TextView) findViewById(R.id.suggestion_for_name); 961 mSuggestionContactsNumber = (TextView) findViewById(R.id.suggestion_for_contacts_number); 962 mSuggestionList = (LinearLayout) findViewById(R.id.suggestion_list); 963 mSuggestionsCancelButton= (Button) findViewById(R.id.cancel_button); 964 mSuggestionsLinkButton = (Button) findViewById(R.id.link_button); 965 if (savedInstanceState != null) { 966 mIsSuggestionListCollapsed = savedInstanceState.getBoolean( 967 KEY_IS_SUGGESTION_LIST_COLLAPSED, true); 968 mPreviousContactId = savedInstanceState.getLong(KEY_PREVIOUS_CONTACT_ID); 969 mSuggestionsShouldAutoSelected = savedInstanceState.getBoolean( 970 KEY_SUGGESTIONS_AUTO_SELECTED, true); 971 mSelectedAggregationIds = (TreeSet<Long>) 972 savedInstanceState.getSerializable(KEY_SELECTED_SUGGESTION_CONTACTS); 973 } else { 974 mIsSuggestionListCollapsed = true; 975 mSelectedAggregationIds.clear(); 976 } 977 if (mSelectedAggregationIds.isEmpty()) { 978 disableLinkButton(); 979 } else { 980 enableLinkButton(); 981 } 982 mCollapasedSuggestionHeader.setOnClickListener(new OnClickListener() { 983 @Override 984 public void onClick(View view) { 985 mCollapsedSuggestionCardView.setVisibility(View.GONE); 986 mExpandSuggestionCardView.setVisibility(View.VISIBLE); 987 mIsSuggestionListCollapsed = false; 988 mExpandSuggestionCardTitle.requestFocus(); 989 mExpandSuggestionCardTitle.sendAccessibilityEvent( 990 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 991 } 992 }); 993 994 mSuggestionsCancelButton.setOnClickListener(new OnClickListener() { 995 @Override 996 public void onClick(View view) { 997 mCollapsedSuggestionCardView.setVisibility(View.VISIBLE); 998 mExpandSuggestionCardView.setVisibility(View.GONE); 999 mIsSuggestionListCollapsed = true; 1000 } 1001 }); 1002 1003 mNoContactDetailsCard.setOnClickListener(mEntryClickHandler); 1004 mContactCard.setOnClickListener(mEntryClickHandler); 1005 mContactCard.setExpandButtonText( 1006 getResources().getString(R.string.expanding_entry_card_view_see_all)); 1007 mContactCard.setOnCreateContextMenuListener(mEntryContextMenuListener); 1008 1009 mRecentCard.setOnClickListener(mEntryClickHandler); 1010 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 1011 1012 mAboutCard.setOnClickListener(mEntryClickHandler); 1013 mAboutCard.setOnCreateContextMenuListener(mEntryContextMenuListener); 1014 1015 mPhotoView = (QuickContactImageView) findViewById(R.id.photo); 1016 final View transparentView = findViewById(R.id.transparent_view); 1017 if (mScroller != null) { 1018 transparentView.setOnClickListener(new OnClickListener() { 1019 @Override 1020 public void onClick(View v) { 1021 mScroller.scrollOffBottom(); 1022 } 1023 }); 1024 } 1025 1026 // Allow a shadow to be shown under the toolbar. 1027 ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources()); 1028 1029 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 1030 setActionBar(toolbar); 1031 getActionBar().setTitle(null); 1032 // Put a TextView with a known resource id into the ActionBar. This allows us to easily 1033 // find the correct TextView location & size later. 1034 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null)); 1035 1036 mHasAlreadyBeenOpened = savedInstanceState != null; 1037 mIsEntranceAnimationFinished = mHasAlreadyBeenOpened; 1038 mWindowScrim = new ColorDrawable(SCRIM_COLOR); 1039 mWindowScrim.setAlpha(0); 1040 getWindow().setBackgroundDrawable(mWindowScrim); 1041 1042 mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED); 1043 // mScroller needs to perform asynchronous measurements after initalize(), therefore 1044 // we can't mark this as GONE. 1045 mScroller.setVisibility(View.INVISIBLE); 1046 1047 setHeaderNameText(R.string.missing_name); 1048 1049 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 1050 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 1051 if (mSelectAccountFragmentListener == null) { 1052 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 1053 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 1054 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 1055 mSelectAccountFragmentListener.setRetainInstance(true); 1056 } 1057 mSelectAccountFragmentListener.setQuickContactActivity(this); 1058 1059 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true, 1060 new Runnable() { 1061 @Override 1062 public void run() { 1063 if (!mHasAlreadyBeenOpened) { 1064 // The initial scrim opacity must match the scrim opacity that would be 1065 // achieved by scrolling to the starting position. 1066 final float alphaRatio = mExtraMode == MODE_FULLY_EXPANDED ? 1067 1 : mScroller.getStartingTransparentHeightRatio(); 1068 final int duration = getResources().getInteger( 1069 android.R.integer.config_shortAnimTime); 1070 final int desiredAlpha = (int) (0xFF * alphaRatio); 1071 ObjectAnimator o = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 1072 desiredAlpha).setDuration(duration); 1073 1074 o.start(); 1075 } 1076 } 1077 }); 1078 1079 if (savedInstanceState != null) { 1080 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0); 1081 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 1082 new Runnable() { 1083 @Override 1084 public void run() { 1085 // Need to wait for the pre draw before setting the initial scroll 1086 // value. Prior to pre draw all scroll values are invalid. 1087 if (mHasAlreadyBeenOpened) { 1088 mScroller.setVisibility(View.VISIBLE); 1089 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 1090 } 1091 // Need to wait for pre draw for setting the theme color. Setting the 1092 // header tint before the MultiShrinkScroller has been measured will 1093 // cause incorrect tinting calculations. 1094 if (color != 0) { 1095 setThemeColor(mMaterialColorMapUtils 1096 .calculatePrimaryAndSecondaryColor(color)); 1097 } 1098 } 1099 }); 1100 } 1101 1102 Trace.endSection(); 1103 } 1104 1105 @Override 1106 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1107 final boolean deletedOrSplit = requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 1108 (resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED || 1109 resultCode == ContactEditorBaseActivity.RESULT_CODE_SPLIT); 1110 if (deletedOrSplit) { 1111 finish(); 1112 } else if (requestCode == REQUEST_CODE_CONTACT_SELECTION_ACTIVITY && 1113 resultCode != RESULT_CANCELED) { 1114 processIntent(data); 1115 } 1116 } 1117 1118 @Override 1119 protected void onNewIntent(Intent intent) { 1120 super.onNewIntent(intent); 1121 mHasAlreadyBeenOpened = true; 1122 mIsEntranceAnimationFinished = true; 1123 mHasComputedThemeColor = false; 1124 processIntent(intent); 1125 } 1126 1127 @Override 1128 public void onSaveInstanceState(Bundle savedInstanceState) { 1129 super.onSaveInstanceState(savedInstanceState); 1130 if (mColorFilter != null) { 1131 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilterColor); 1132 } 1133 savedInstanceState.putBoolean(KEY_IS_SUGGESTION_LIST_COLLAPSED, mIsSuggestionListCollapsed); 1134 savedInstanceState.putLong(KEY_PREVIOUS_CONTACT_ID, mPreviousContactId); 1135 savedInstanceState.putBoolean( 1136 KEY_SUGGESTIONS_AUTO_SELECTED, mSuggestionsShouldAutoSelected); 1137 savedInstanceState.putSerializable( 1138 KEY_SELECTED_SUGGESTION_CONTACTS, mSelectedAggregationIds); 1139 } 1140 1141 private void processIntent(Intent intent) { 1142 if (intent == null) { 1143 finish(); 1144 return; 1145 } 1146 Uri lookupUri = intent.getData(); 1147 1148 // Check to see whether it comes from the old version. 1149 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 1150 final long rawContactId = ContentUris.parseId(lookupUri); 1151 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 1152 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 1153 } 1154 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, QuickContact.MODE_LARGE); 1155 mExtraPrioritizedMimeType = getIntent().getStringExtra(QuickContact.EXTRA_PRIORITIZED_MIMETYPE); 1156 final Uri oldLookupUri = mLookupUri; 1157 1158 if (lookupUri == null) { 1159 finish(); 1160 return; 1161 } 1162 mLookupUri = lookupUri; 1163 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 1164 if (oldLookupUri == null) { 1165 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 1166 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 1167 } else if (oldLookupUri != mLookupUri) { 1168 // After copying a directory contact, the contact URI changes. Therefore, 1169 // we need to reload the new contact. 1170 destroyInteractionLoaders(); 1171 mContactLoader = (ContactLoader) (Loader<?>) getLoaderManager().getLoader( 1172 LOADER_CONTACT_ID); 1173 mCachedCp2DataCardModel = null; 1174 } 1175 mContactLoader.forceLoad(); 1176 1177 NfcHandler.register(this, mLookupUri); 1178 } 1179 1180 private void destroyInteractionLoaders() { 1181 for (int interactionLoaderId : mRecentLoaderIds) { 1182 getLoaderManager().destroyLoader(interactionLoaderId); 1183 } 1184 } 1185 1186 private void runEntranceAnimation() { 1187 if (mHasAlreadyBeenOpened) { 1188 return; 1189 } 1190 mHasAlreadyBeenOpened = true; 1191 mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED); 1192 } 1193 1194 /** Assign this string to the view if it is not empty. */ 1195 private void setHeaderNameText(int resId) { 1196 if (mScroller != null) { 1197 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString(), 1198 /* isPhoneNumber= */ false); 1199 } 1200 } 1201 1202 /** Assign this string to the view if it is not empty. */ 1203 private void setHeaderNameText(String value, boolean isPhoneNumber) { 1204 if (!TextUtils.isEmpty(value)) { 1205 if (mScroller != null) { 1206 mScroller.setTitle(value, isPhoneNumber); 1207 } 1208 } 1209 } 1210 1211 /** 1212 * Check if the given MIME-type appears in the list of excluded MIME-types 1213 * that the most-recent caller requested. 1214 */ 1215 private boolean isMimeExcluded(String mimeType) { 1216 if (mExcludeMimes == null) return false; 1217 for (String excludedMime : mExcludeMimes) { 1218 if (TextUtils.equals(excludedMime, mimeType)) { 1219 return true; 1220 } 1221 } 1222 return false; 1223 } 1224 1225 /** 1226 * Handle the result from the ContactLoader 1227 */ 1228 private void bindContactData(final Contact data) { 1229 Trace.beginSection("bindContactData"); 1230 mContactData = data; 1231 invalidateOptionsMenu(); 1232 1233 Trace.endSection(); 1234 Trace.beginSection("Set display photo & name"); 1235 1236 mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization()); 1237 mPhotoSetter.setupContactPhoto(data, mPhotoView); 1238 extractAndApplyTintFromPhotoViewAsynchronously(); 1239 final String displayName = ContactDisplayUtils.getDisplayName(this, data).toString(); 1240 setHeaderNameText( 1241 displayName, mContactData.getDisplayNameSource() == DisplayNameSources.PHONE); 1242 final String phoneticName = ContactDisplayUtils.getPhoneticName(this, data); 1243 if (mScroller != null) { 1244 if (mContactData.getDisplayNameSource() != DisplayNameSources.STRUCTURED_PHONETIC_NAME 1245 && !TextUtils.isEmpty(phoneticName)) { 1246 mScroller.setPhoneticName(phoneticName); 1247 } else { 1248 mScroller.setPhoneticNameGone(); 1249 } 1250 } 1251 1252 Trace.endSection(); 1253 1254 mEntriesAndActionsTask = new AsyncTask<Void, Void, Cp2DataCardModel>() { 1255 1256 @Override 1257 protected Cp2DataCardModel doInBackground( 1258 Void... params) { 1259 return generateDataModelFromContact(data); 1260 } 1261 1262 @Override 1263 protected void onPostExecute(Cp2DataCardModel cardDataModel) { 1264 super.onPostExecute(cardDataModel); 1265 // Check that original AsyncTask parameters are still valid and the activity 1266 // is still running before binding to UI. A new intent could invalidate 1267 // the results, for example. 1268 if (data == mContactData && !isCancelled()) { 1269 bindDataToCards(cardDataModel); 1270 showActivity(); 1271 } 1272 } 1273 }; 1274 mEntriesAndActionsTask.execute(); 1275 } 1276 1277 private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) { 1278 startInteractionLoaders(cp2DataCardModel); 1279 populateContactAndAboutCard(cp2DataCardModel); 1280 populateSuggestionCard(); 1281 } 1282 1283 private void startInteractionLoaders(Cp2DataCardModel cp2DataCardModel) { 1284 final Map<String, List<DataItem>> dataItemsMap = cp2DataCardModel.dataItemsMap; 1285 final List<DataItem> phoneDataItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE); 1286 if (phoneDataItems != null && phoneDataItems.size() == 1) { 1287 mOnlyOnePhoneNumber = true; 1288 } 1289 String[] phoneNumbers = null; 1290 if (phoneDataItems != null) { 1291 phoneNumbers = new String[phoneDataItems.size()]; 1292 for (int i = 0; i < phoneDataItems.size(); ++i) { 1293 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber(); 1294 } 1295 } 1296 final Bundle phonesExtraBundle = new Bundle(); 1297 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers); 1298 1299 Trace.beginSection("start sms loader"); 1300 getLoaderManager().initLoader( 1301 LOADER_SMS_ID, 1302 phonesExtraBundle, 1303 mLoaderInteractionsCallbacks); 1304 Trace.endSection(); 1305 1306 Trace.beginSection("start call log loader"); 1307 getLoaderManager().initLoader( 1308 LOADER_CALL_LOG_ID, 1309 phonesExtraBundle, 1310 mLoaderInteractionsCallbacks); 1311 Trace.endSection(); 1312 1313 1314 Trace.beginSection("start calendar loader"); 1315 final List<DataItem> emailDataItems = dataItemsMap.get(Email.CONTENT_ITEM_TYPE); 1316 if (emailDataItems != null && emailDataItems.size() == 1) { 1317 mOnlyOneEmail = true; 1318 } 1319 String[] emailAddresses = null; 1320 if (emailDataItems != null) { 1321 emailAddresses = new String[emailDataItems.size()]; 1322 for (int i = 0; i < emailDataItems.size(); ++i) { 1323 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress(); 1324 } 1325 } 1326 final Bundle emailsExtraBundle = new Bundle(); 1327 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses); 1328 getLoaderManager().initLoader( 1329 LOADER_CALENDAR_ID, 1330 emailsExtraBundle, 1331 mLoaderInteractionsCallbacks); 1332 Trace.endSection(); 1333 } 1334 1335 private void showActivity() { 1336 if (mScroller != null) { 1337 mScroller.setVisibility(View.VISIBLE); 1338 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 1339 new Runnable() { 1340 @Override 1341 public void run() { 1342 runEntranceAnimation(); 1343 } 1344 }); 1345 } 1346 } 1347 1348 private List<List<Entry>> buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap) { 1349 final List<List<Entry>> aboutCardEntries = new ArrayList<>(); 1350 for (String mimetype : SORTED_ABOUT_CARD_MIMETYPES) { 1351 final List<DataItem> mimeTypeItems = dataItemsMap.get(mimetype); 1352 if (mimeTypeItems == null) { 1353 continue; 1354 } 1355 // Set aboutCardTitleOut = null, since SORTED_ABOUT_CARD_MIMETYPES doesn't contain 1356 // the name mimetype. 1357 final List<Entry> aboutEntries = dataItemsToEntries(mimeTypeItems, 1358 /* aboutCardTitleOut = */ null); 1359 if (aboutEntries.size() > 0) { 1360 aboutCardEntries.add(aboutEntries); 1361 } 1362 } 1363 return aboutCardEntries; 1364 } 1365 1366 @Override 1367 protected void onResume() { 1368 super.onResume(); 1369 // If returning from a launched activity, repopulate the contact and about card 1370 if (mHasIntentLaunched) { 1371 mHasIntentLaunched = false; 1372 populateContactAndAboutCard(mCachedCp2DataCardModel); 1373 } 1374 1375 // When exiting the activity and resuming, we want to force a full reload of all the 1376 // interaction data in case something changed in the background. On screen rotation, 1377 // we don't need to do this. And, mCachedCp2DataCardModel will be null, so we won't. 1378 if (mCachedCp2DataCardModel != null) { 1379 destroyInteractionLoaders(); 1380 startInteractionLoaders(mCachedCp2DataCardModel); 1381 } 1382 } 1383 1384 private void populateSuggestionCard() { 1385 // Initialize suggestion related view and data. 1386 if (mPreviousContactId != mContactData.getId()) { 1387 mCollapsedSuggestionCardView.setVisibility(View.GONE); 1388 mExpandSuggestionCardView.setVisibility(View.GONE); 1389 mIsSuggestionListCollapsed = true; 1390 mSuggestionsShouldAutoSelected = true; 1391 mSuggestionList.removeAllViews(); 1392 } 1393 1394 // Do not show the card when it's directory contact or invisible. 1395 if (DirectoryContactUtil.isDirectoryContact(mContactData) 1396 || InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1397 return; 1398 } 1399 1400 if (mAggregationSuggestionEngine == null) { 1401 mAggregationSuggestionEngine = new AggregationSuggestionEngine(this); 1402 mAggregationSuggestionEngine.setListener(this); 1403 mAggregationSuggestionEngine.setSuggestionsLimit(getResources().getInteger( 1404 R.integer.quickcontact_suggestions_limit)); 1405 mAggregationSuggestionEngine.start(); 1406 } 1407 1408 mAggregationSuggestionEngine.setContactId(mContactData.getId()); 1409 if (mPreviousContactId != 0 1410 && mPreviousContactId != mContactData.getId()) { 1411 // Clear selected Ids when listing suggestions for new contact Id. 1412 mSelectedAggregationIds.clear(); 1413 } 1414 mPreviousContactId = mContactData.getId(); 1415 1416 // Trigger suggestion engine to compute suggestions. 1417 if (mContactData.getId() <= 0) { 1418 return; 1419 } 1420 final ContentValues values = new ContentValues(); 1421 values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, 1422 mContactData.getDisplayName()); 1423 values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, 1424 mContactData.getPhoneticName()); 1425 mAggregationSuggestionEngine.onNameChange(ValuesDelta.fromBefore(values)); 1426 } 1427 1428 private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel) { 1429 mCachedCp2DataCardModel = cp2DataCardModel; 1430 if (mHasIntentLaunched || cp2DataCardModel == null) { 1431 return; 1432 } 1433 Trace.beginSection("bind contact card"); 1434 1435 final List<List<Entry>> contactCardEntries = cp2DataCardModel.contactCardEntries; 1436 final List<List<Entry>> aboutCardEntries = cp2DataCardModel.aboutCardEntries; 1437 final String customAboutCardName = cp2DataCardModel.customAboutCardName; 1438 1439 if (contactCardEntries.size() > 0) { 1440 final boolean firstEntriesArePrioritizedMimeType = 1441 !TextUtils.isEmpty(mExtraPrioritizedMimeType) && 1442 mCachedCp2DataCardModel.dataItemsMap.containsKey(mExtraPrioritizedMimeType) && 1443 mCachedCp2DataCardModel.dataItemsMap.get(mExtraPrioritizedMimeType).size() != 0; 1444 mContactCard.initialize(contactCardEntries, 1445 /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN, 1446 /* isExpanded = */ mContactCard.isExpanded(), 1447 /* isAlwaysExpanded = */ false, 1448 mExpandingEntryCardViewListener, 1449 mScroller, 1450 firstEntriesArePrioritizedMimeType); 1451 mContactCard.setVisibility(View.VISIBLE); 1452 } else { 1453 mContactCard.setVisibility(View.GONE); 1454 } 1455 Trace.endSection(); 1456 1457 Trace.beginSection("bind about card"); 1458 // Phonetic name is not a data item, so the entry needs to be created separately 1459 final String phoneticName = mContactData.getPhoneticName(); 1460 if (!TextUtils.isEmpty(phoneticName)) { 1461 Entry phoneticEntry = new Entry(/* viewId = */ -1, 1462 /* icon = */ null, 1463 getResources().getString(R.string.name_phonetic), 1464 phoneticName, 1465 /* subHeaderIcon = */ null, 1466 /* text = */ null, 1467 /* textIcon = */ null, 1468 /* primaryContentDescription = */ null, 1469 /* intent = */ null, 1470 /* alternateIcon = */ null, 1471 /* alternateIntent = */ null, 1472 /* alternateContentDescription = */ null, 1473 /* shouldApplyColor = */ false, 1474 /* isEditable = */ false, 1475 /* EntryContextMenuInfo = */ new EntryContextMenuInfo(phoneticName, 1476 getResources().getString(R.string.name_phonetic), 1477 /* mimeType = */ null, /* id = */ -1, /* isPrimary = */ false), 1478 /* thirdIcon = */ null, 1479 /* thirdIntent = */ null, 1480 /* thirdContentDescription = */ null, 1481 /* thirdAction = */ Entry.ACTION_NONE, 1482 /* thirdExtras = */ null, 1483 /* iconResourceId = */ 0); 1484 List<Entry> phoneticList = new ArrayList<>(); 1485 phoneticList.add(phoneticEntry); 1486 // Phonetic name comes after nickname. Check to see if the first entry type is nickname 1487 if (aboutCardEntries.size() > 0 && aboutCardEntries.get(0).get(0).getHeader().equals( 1488 getResources().getString(R.string.header_nickname_entry))) { 1489 aboutCardEntries.add(1, phoneticList); 1490 } else { 1491 aboutCardEntries.add(0, phoneticList); 1492 } 1493 } 1494 1495 if (!TextUtils.isEmpty(customAboutCardName)) { 1496 mAboutCard.setTitle(customAboutCardName); 1497 } 1498 1499 mAboutCard.initialize(aboutCardEntries, 1500 /* numInitialVisibleEntries = */ 1, 1501 /* isExpanded = */ true, 1502 /* isAlwaysExpanded = */ true, 1503 mExpandingEntryCardViewListener, 1504 mScroller); 1505 1506 if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) { 1507 initializeNoContactDetailCard(); 1508 } else { 1509 mNoContactDetailsCard.setVisibility(View.GONE); 1510 } 1511 1512 // If the Recent card is already initialized (all recent data is loaded), show the About 1513 // card if it has entries. Otherwise About card visibility will be set in bindRecentData() 1514 if (isAllRecentDataLoaded() && aboutCardEntries.size() > 0) { 1515 mAboutCard.setVisibility(View.VISIBLE); 1516 } 1517 Trace.endSection(); 1518 } 1519 1520 /** 1521 * Create a card that shows "Add email" and "Add phone number" entries in grey. 1522 */ 1523 private void initializeNoContactDetailCard() { 1524 final Drawable phoneIcon = getResources().getDrawable( 1525 R.drawable.ic_phone_24dp).mutate(); 1526 final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 1527 phoneIcon, getString(R.string.quickcontact_add_phone_number), 1528 /* subHeader = */ null, /* subHeaderIcon = */ null, /* text = */ null, 1529 /* textIcon = */ null, /* primaryContentDescription = */ null, 1530 getEditContactIntent(), 1531 /* alternateIcon = */ null, /* alternateIntent = */ null, 1532 /* alternateContentDescription = */ null, /* shouldApplyColor = */ true, 1533 /* isEditable = */ false, /* EntryContextMenuInfo = */ null, 1534 /* thirdIcon = */ null, /* thirdIntent = */ null, 1535 /* thirdContentDescription = */ null, 1536 /* thirdAction = */ Entry.ACTION_NONE, 1537 /* thirdExtras = */ null, 1538 R.drawable.ic_phone_24dp); 1539 1540 final Drawable emailIcon = getResources().getDrawable( 1541 R.drawable.ic_email_24dp).mutate(); 1542 final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 1543 emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null, 1544 /* subHeaderIcon = */ null, 1545 /* text = */ null, /* textIcon = */ null, /* primaryContentDescription = */ null, 1546 getEditContactIntent(), /* alternateIcon = */ null, 1547 /* alternateIntent = */ null, /* alternateContentDescription = */ null, 1548 /* shouldApplyColor = */ true, /* isEditable = */ false, 1549 /* EntryContextMenuInfo = */ null, /* thirdIcon = */ null, 1550 /* thirdIntent = */ null, /* thirdContentDescription = */ null, 1551 /* thirdAction = */ Entry.ACTION_NONE, /* thirdExtras = */ null, 1552 R.drawable.ic_email_24dp); 1553 1554 final List<List<Entry>> promptEntries = new ArrayList<>(); 1555 promptEntries.add(new ArrayList<Entry>(1)); 1556 promptEntries.add(new ArrayList<Entry>(1)); 1557 promptEntries.get(0).add(phonePromptEntry); 1558 promptEntries.get(1).add(emailPromptEntry); 1559 1560 final int subHeaderTextColor = getResources().getColor( 1561 R.color.quickcontact_entry_sub_header_text_color); 1562 final PorterDuffColorFilter greyColorFilter = 1563 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP); 1564 mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ true, 1565 /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener, mScroller); 1566 mNoContactDetailsCard.setVisibility(View.VISIBLE); 1567 mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor); 1568 mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter); 1569 } 1570 1571 /** 1572 * Builds the {@link DataItem}s Map out of the Contact. 1573 * @param data The contact to build the data from. 1574 * @return A pair containing a list of data items sorted within mimetype and sorted 1575 * amongst mimetype. The map goes from mimetype string to the sorted list of data items within 1576 * mimetype 1577 */ 1578 private Cp2DataCardModel generateDataModelFromContact( 1579 Contact data) { 1580 Trace.beginSection("Build data items map"); 1581 1582 final Map<String, List<DataItem>> dataItemsMap = new HashMap<>(); 1583 1584 final ResolveCache cache = ResolveCache.getInstance(this); 1585 for (RawContact rawContact : data.getRawContacts()) { 1586 for (DataItem dataItem : rawContact.getDataItems()) { 1587 dataItem.setRawContactId(rawContact.getId()); 1588 1589 final String mimeType = dataItem.getMimeType(); 1590 if (mimeType == null) continue; 1591 1592 final AccountType accountType = rawContact.getAccountType(this); 1593 final DataKind dataKind = AccountTypeManager.getInstance(this) 1594 .getKindOrFallback(accountType, mimeType); 1595 if (dataKind == null) continue; 1596 1597 dataItem.setDataKind(dataKind); 1598 1599 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this, 1600 dataKind)); 1601 1602 if (isMimeExcluded(mimeType) || !hasData) continue; 1603 1604 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType); 1605 if (dataItemListByType == null) { 1606 dataItemListByType = new ArrayList<>(); 1607 dataItemsMap.put(mimeType, dataItemListByType); 1608 } 1609 dataItemListByType.add(dataItem); 1610 } 1611 } 1612 Trace.endSection(); 1613 1614 Trace.beginSection("sort within mimetypes"); 1615 /* 1616 * Sorting is a multi part step. The end result is to a have a sorted list of the most 1617 * used data items, one per mimetype. Then, within each mimetype, the list of data items 1618 * for that type is also sorted, based off of {super primary, primary, times used} in that 1619 * order. 1620 */ 1621 final List<List<DataItem>> dataItemsList = new ArrayList<>(); 1622 for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) { 1623 // Remove duplicate data items 1624 Collapser.collapseList(mimeTypeDataItems, this); 1625 // Sort within mimetype 1626 Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator); 1627 // Add to the list of data item lists 1628 dataItemsList.add(mimeTypeDataItems); 1629 } 1630 Trace.endSection(); 1631 1632 Trace.beginSection("sort amongst mimetypes"); 1633 // Sort amongst mimetypes to bubble up the top data items for the contact card 1634 Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator); 1635 Trace.endSection(); 1636 1637 Trace.beginSection("cp2 data items to entries"); 1638 1639 final List<List<Entry>> contactCardEntries = new ArrayList<>(); 1640 final List<List<Entry>> aboutCardEntries = buildAboutCardEntries(dataItemsMap); 1641 final MutableString aboutCardName = new MutableString(); 1642 1643 for (int i = 0; i < dataItemsList.size(); ++i) { 1644 final List<DataItem> dataItemsByMimeType = dataItemsList.get(i); 1645 final DataItem topDataItem = dataItemsByMimeType.get(0); 1646 if (SORTED_ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) { 1647 // About card mimetypes are built in buildAboutCardEntries, skip here 1648 continue; 1649 } else { 1650 List<Entry> contactEntries = dataItemsToEntries(dataItemsList.get(i), 1651 aboutCardName); 1652 if (contactEntries.size() > 0) { 1653 contactCardEntries.add(contactEntries); 1654 } 1655 } 1656 } 1657 1658 Trace.endSection(); 1659 1660 final Cp2DataCardModel dataModel = new Cp2DataCardModel(); 1661 dataModel.customAboutCardName = aboutCardName.value; 1662 dataModel.aboutCardEntries = aboutCardEntries; 1663 dataModel.contactCardEntries = contactCardEntries; 1664 dataModel.dataItemsMap = dataItemsMap; 1665 return dataModel; 1666 } 1667 1668 /** 1669 * Class used to hold the About card and Contact cards' data model that gets generated 1670 * on a background thread. All data is from CP2. 1671 */ 1672 private static class Cp2DataCardModel { 1673 /** 1674 * A map between a mimetype string and the corresponding list of data items. The data items 1675 * are in sorted order using mWithinMimeTypeDataItemComparator. 1676 */ 1677 public Map<String, List<DataItem>> dataItemsMap; 1678 public List<List<Entry>> aboutCardEntries; 1679 public List<List<Entry>> contactCardEntries; 1680 public String customAboutCardName; 1681 } 1682 1683 private static class MutableString { 1684 public String value; 1685 } 1686 1687 /** 1688 * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display. 1689 * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned. 1690 * 1691 * This runs on a background thread. This is set as static to avoid accidentally adding 1692 * additional dependencies on unsafe things (like the Activity). 1693 * 1694 * @param dataItem The {@link DataItem} to convert. 1695 * @param secondDataItem A second {@link DataItem} to help build a full entry for some 1696 * mimetypes 1697 * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present. 1698 */ 1699 private static Entry dataItemToEntry(DataItem dataItem, DataItem secondDataItem, 1700 Context context, Contact contactData, 1701 final MutableString aboutCardName) { 1702 Drawable icon = null; 1703 String header = null; 1704 String subHeader = null; 1705 Drawable subHeaderIcon = null; 1706 String text = null; 1707 Drawable textIcon = null; 1708 StringBuilder primaryContentDescription = new StringBuilder(); 1709 Spannable phoneContentDescription = null; 1710 Spannable smsContentDescription = null; 1711 Intent intent = null; 1712 boolean shouldApplyColor = true; 1713 Drawable alternateIcon = null; 1714 Intent alternateIntent = null; 1715 StringBuilder alternateContentDescription = new StringBuilder(); 1716 final boolean isEditable = false; 1717 EntryContextMenuInfo entryContextMenuInfo = null; 1718 Drawable thirdIcon = null; 1719 Intent thirdIntent = null; 1720 int thirdAction = Entry.ACTION_NONE; 1721 String thirdContentDescription = null; 1722 Bundle thirdExtras = null; 1723 int iconResourceId = 0; 1724 1725 context = context.getApplicationContext(); 1726 final Resources res = context.getResources(); 1727 DataKind kind = dataItem.getDataKind(); 1728 1729 if (dataItem instanceof ImDataItem) { 1730 final ImDataItem im = (ImDataItem) dataItem; 1731 intent = ContactsUtils.buildImIntent(context, im).first; 1732 final boolean isEmail = im.isCreatedFromEmail(); 1733 final int protocol; 1734 if (!im.isProtocolValid()) { 1735 protocol = Im.PROTOCOL_CUSTOM; 1736 } else { 1737 protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); 1738 } 1739 if (protocol == Im.PROTOCOL_CUSTOM) { 1740 // If the protocol is custom, display the "IM" entry header as well to distinguish 1741 // this entry from other ones 1742 header = res.getString(R.string.header_im_entry); 1743 subHeader = Im.getProtocolLabel(res, protocol, 1744 im.getCustomProtocol()).toString(); 1745 text = im.getData(); 1746 } else { 1747 header = Im.getProtocolLabel(res, protocol, 1748 im.getCustomProtocol()).toString(); 1749 subHeader = im.getData(); 1750 } 1751 entryContextMenuInfo = new EntryContextMenuInfo(im.getData(), header, 1752 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1753 } else if (dataItem instanceof OrganizationDataItem) { 1754 final OrganizationDataItem organization = (OrganizationDataItem) dataItem; 1755 header = res.getString(R.string.header_organization_entry); 1756 subHeader = organization.getCompany(); 1757 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1758 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1759 text = organization.getTitle(); 1760 } else if (dataItem instanceof NicknameDataItem) { 1761 final NicknameDataItem nickname = (NicknameDataItem) dataItem; 1762 // Build nickname entries 1763 final boolean isNameRawContact = 1764 (contactData.getNameRawContactId() == dataItem.getRawContactId()); 1765 1766 final boolean duplicatesTitle = 1767 isNameRawContact 1768 && contactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 1769 1770 if (!duplicatesTitle) { 1771 header = res.getString(R.string.header_nickname_entry); 1772 subHeader = nickname.getName(); 1773 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1774 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1775 } 1776 } else if (dataItem instanceof NoteDataItem) { 1777 final NoteDataItem note = (NoteDataItem) dataItem; 1778 header = res.getString(R.string.header_note_entry); 1779 subHeader = note.getNote(); 1780 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1781 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1782 } else if (dataItem instanceof WebsiteDataItem) { 1783 final WebsiteDataItem website = (WebsiteDataItem) dataItem; 1784 header = res.getString(R.string.header_website_entry); 1785 subHeader = website.getUrl(); 1786 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1787 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1788 try { 1789 final WebAddress webAddress = new WebAddress(website.buildDataStringForDisplay 1790 (context, kind)); 1791 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString())); 1792 } catch (final ParseException e) { 1793 Log.e(TAG, "Couldn't parse website: " + website.buildDataStringForDisplay( 1794 context, kind)); 1795 } 1796 } else if (dataItem instanceof EventDataItem) { 1797 final EventDataItem event = (EventDataItem) dataItem; 1798 final String dataString = event.buildDataStringForDisplay(context, kind); 1799 final Calendar cal = DateUtils.parseDate(dataString, false); 1800 if (cal != null) { 1801 final Date nextAnniversary = 1802 DateUtils.getNextAnnualDate(cal); 1803 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 1804 builder.appendPath("time"); 1805 ContentUris.appendId(builder, nextAnniversary.getTime()); 1806 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); 1807 } 1808 header = res.getString(R.string.header_event_entry); 1809 if (event.hasKindTypeColumn(kind)) { 1810 subHeader = EventCompat.getTypeLabel(res, event.getKindTypeColumn(kind), 1811 event.getLabel()).toString(); 1812 } 1813 text = DateUtils.formatDate(context, dataString); 1814 entryContextMenuInfo = new EntryContextMenuInfo(text, header, 1815 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1816 } else if (dataItem instanceof RelationDataItem) { 1817 final RelationDataItem relation = (RelationDataItem) dataItem; 1818 final String dataString = relation.buildDataStringForDisplay(context, kind); 1819 if (!TextUtils.isEmpty(dataString)) { 1820 intent = new Intent(Intent.ACTION_SEARCH); 1821 intent.putExtra(SearchManager.QUERY, dataString); 1822 intent.setType(Contacts.CONTENT_TYPE); 1823 } 1824 header = res.getString(R.string.header_relation_entry); 1825 subHeader = relation.getName(); 1826 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1827 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1828 if (relation.hasKindTypeColumn(kind)) { 1829 text = Relation.getTypeLabel(res, 1830 relation.getKindTypeColumn(kind), 1831 relation.getLabel()).toString(); 1832 } 1833 } else if (dataItem instanceof PhoneDataItem) { 1834 final PhoneDataItem phone = (PhoneDataItem) dataItem; 1835 String phoneLabel = null; 1836 if (!TextUtils.isEmpty(phone.getNumber())) { 1837 primaryContentDescription.append(res.getString(R.string.call_other)).append(" "); 1838 header = sBidiFormatter.unicodeWrap(phone.buildDataStringForDisplay(context, kind), 1839 TextDirectionHeuristics.LTR); 1840 entryContextMenuInfo = new EntryContextMenuInfo(header, 1841 res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(), 1842 dataItem.getId(), dataItem.isSuperPrimary()); 1843 if (phone.hasKindTypeColumn(kind)) { 1844 final int kindTypeColumn = phone.getKindTypeColumn(kind); 1845 final String label = phone.getLabel(); 1846 phoneLabel = label; 1847 if (kindTypeColumn == Phone.TYPE_CUSTOM && TextUtils.isEmpty(label)) { 1848 text = ""; 1849 } else { 1850 text = Phone.getTypeLabel(res, kindTypeColumn, label).toString(); 1851 phoneLabel= text; 1852 primaryContentDescription.append(text).append(" "); 1853 } 1854 } 1855 primaryContentDescription.append(header); 1856 phoneContentDescription = com.android.contacts.common.util.ContactDisplayUtils 1857 .getTelephoneTtsSpannable(primaryContentDescription.toString(), header); 1858 icon = res.getDrawable(R.drawable.ic_phone_24dp); 1859 iconResourceId = R.drawable.ic_phone_24dp; 1860 if (PhoneCapabilityTester.isPhone(context)) { 1861 intent = CallUtil.getCallIntent(phone.getNumber()); 1862 } 1863 alternateIntent = new Intent(Intent.ACTION_SENDTO, 1864 Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phone.getNumber(), null)); 1865 1866 alternateIcon = res.getDrawable(R.drawable.ic_message_24dp); 1867 alternateContentDescription.append(res.getString(R.string.sms_custom, header)); 1868 smsContentDescription = com.android.contacts.common.util.ContactDisplayUtils 1869 .getTelephoneTtsSpannable(alternateContentDescription.toString(), header); 1870 1871 int videoCapability = CallUtil.getVideoCallingAvailability(context); 1872 boolean isPresenceEnabled = 1873 (videoCapability & CallUtil.VIDEO_CALLING_PRESENCE) != 0; 1874 boolean isVideoEnabled = (videoCapability & CallUtil.VIDEO_CALLING_ENABLED) != 0; 1875 1876 if (CallUtil.isCallWithSubjectSupported(context)) { 1877 thirdIcon = res.getDrawable(R.drawable.ic_call_note_white_24dp); 1878 thirdAction = Entry.ACTION_CALL_WITH_SUBJECT; 1879 thirdContentDescription = 1880 res.getString(R.string.call_with_a_note); 1881 // Create a bundle containing the data the call subject dialog requires. 1882 thirdExtras = new Bundle(); 1883 thirdExtras.putLong(CallSubjectDialog.ARG_PHOTO_ID, 1884 contactData.getPhotoId()); 1885 thirdExtras.putParcelable(CallSubjectDialog.ARG_PHOTO_URI, 1886 UriUtils.parseUriOrNull(contactData.getPhotoUri())); 1887 thirdExtras.putParcelable(CallSubjectDialog.ARG_CONTACT_URI, 1888 contactData.getLookupUri()); 1889 thirdExtras.putString(CallSubjectDialog.ARG_NAME_OR_NUMBER, 1890 contactData.getDisplayName()); 1891 thirdExtras.putBoolean(CallSubjectDialog.ARG_IS_BUSINESS, false); 1892 thirdExtras.putString(CallSubjectDialog.ARG_NUMBER, 1893 phone.getNumber()); 1894 thirdExtras.putString(CallSubjectDialog.ARG_DISPLAY_NUMBER, 1895 phone.getFormattedPhoneNumber()); 1896 thirdExtras.putString(CallSubjectDialog.ARG_NUMBER_LABEL, 1897 phoneLabel); 1898 } else if (isVideoEnabled) { 1899 // Check to ensure carrier presence indicates the number supports video calling. 1900 int carrierPresence = dataItem.getCarrierPresence(); 1901 boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; 1902 1903 if ((isPresenceEnabled && isPresent) || !isPresenceEnabled) { 1904 thirdIcon = res.getDrawable(R.drawable.ic_videocam); 1905 thirdAction = Entry.ACTION_INTENT; 1906 thirdIntent = CallUtil.getVideoCallIntent(phone.getNumber(), 1907 CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY); 1908 thirdContentDescription = 1909 res.getString(R.string.description_video_call); 1910 } 1911 } 1912 } 1913 } else if (dataItem instanceof EmailDataItem) { 1914 final EmailDataItem email = (EmailDataItem) dataItem; 1915 final String address = email.getData(); 1916 if (!TextUtils.isEmpty(address)) { 1917 primaryContentDescription.append(res.getString(R.string.email_other)).append(" "); 1918 final Uri mailUri = Uri.fromParts(ContactsUtils.SCHEME_MAILTO, address, null); 1919 intent = new Intent(Intent.ACTION_SENDTO, mailUri); 1920 header = email.getAddress(); 1921 entryContextMenuInfo = new EntryContextMenuInfo(header, 1922 res.getString(R.string.emailLabelsGroup), dataItem.getMimeType(), 1923 dataItem.getId(), dataItem.isSuperPrimary()); 1924 if (email.hasKindTypeColumn(kind)) { 1925 text = Email.getTypeLabel(res, email.getKindTypeColumn(kind), 1926 email.getLabel()).toString(); 1927 primaryContentDescription.append(text).append(" "); 1928 } 1929 primaryContentDescription.append(header); 1930 icon = res.getDrawable(R.drawable.ic_email_24dp); 1931 iconResourceId = R.drawable.ic_email_24dp; 1932 } 1933 } else if (dataItem instanceof StructuredPostalDataItem) { 1934 StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem; 1935 final String postalAddress = postal.getFormattedAddress(); 1936 if (!TextUtils.isEmpty(postalAddress)) { 1937 primaryContentDescription.append(res.getString(R.string.map_other)).append(" "); 1938 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress); 1939 header = postal.getFormattedAddress(); 1940 entryContextMenuInfo = new EntryContextMenuInfo(header, 1941 res.getString(R.string.postalLabelsGroup), dataItem.getMimeType(), 1942 dataItem.getId(), dataItem.isSuperPrimary()); 1943 if (postal.hasKindTypeColumn(kind)) { 1944 text = StructuredPostal.getTypeLabel(res, 1945 postal.getKindTypeColumn(kind), postal.getLabel()).toString(); 1946 primaryContentDescription.append(text).append(" "); 1947 } 1948 primaryContentDescription.append(header); 1949 alternateIntent = 1950 StructuredPostalUtils.getViewPostalAddressDirectionsIntent(postalAddress); 1951 alternateIcon = res.getDrawable(R.drawable.ic_directions_24dp); 1952 alternateContentDescription.append(res.getString( 1953 R.string.content_description_directions)).append(" ").append(header); 1954 icon = res.getDrawable(R.drawable.ic_place_24dp); 1955 iconResourceId = R.drawable.ic_place_24dp; 1956 } 1957 } else if (dataItem instanceof SipAddressDataItem) { 1958 final SipAddressDataItem sip = (SipAddressDataItem) dataItem; 1959 final String address = sip.getSipAddress(); 1960 if (!TextUtils.isEmpty(address)) { 1961 primaryContentDescription.append(res.getString(R.string.call_other)).append( 1962 " "); 1963 if (PhoneCapabilityTester.isSipPhone(context)) { 1964 final Uri callUri = Uri.fromParts(PhoneAccount.SCHEME_SIP, address, null); 1965 intent = CallUtil.getCallIntent(callUri); 1966 } 1967 header = address; 1968 entryContextMenuInfo = new EntryContextMenuInfo(header, 1969 res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(), 1970 dataItem.getId(), dataItem.isSuperPrimary()); 1971 if (sip.hasKindTypeColumn(kind)) { 1972 text = SipAddress.getTypeLabel(res, 1973 sip.getKindTypeColumn(kind), sip.getLabel()).toString(); 1974 primaryContentDescription.append(text).append(" "); 1975 } 1976 primaryContentDescription.append(header); 1977 icon = res.getDrawable(R.drawable.ic_dialer_sip_black_24dp); 1978 iconResourceId = R.drawable.ic_dialer_sip_black_24dp; 1979 } 1980 } else if (dataItem instanceof StructuredNameDataItem) { 1981 // If the name is already set and this is not the super primary value then leave the 1982 // current value. This way we show the super primary value when we are able to. 1983 if (dataItem.isSuperPrimary() || aboutCardName.value == null 1984 || aboutCardName.value.isEmpty()) { 1985 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName(); 1986 if (!TextUtils.isEmpty(givenName)) { 1987 aboutCardName.value = res.getString(R.string.about_card_title) + 1988 " " + givenName; 1989 } else { 1990 aboutCardName.value = res.getString(R.string.about_card_title); 1991 } 1992 } 1993 } else { 1994 // Custom DataItem 1995 header = dataItem.buildDataStringForDisplay(context, kind); 1996 text = kind.typeColumn; 1997 intent = new Intent(Intent.ACTION_VIEW); 1998 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId()); 1999 intent.setDataAndType(uri, dataItem.getMimeType()); 2000 2001 if (intent != null) { 2002 final String mimetype = intent.getType(); 2003 2004 // Build advanced entry for known 3p types. Otherwise default to ResolveCache icon. 2005 switch (mimetype) { 2006 case MIMETYPE_GPLUS_PROFILE: 2007 // If a secondDataItem is available, use it to build an entry with 2008 // alternate actions 2009 if (secondDataItem != null) { 2010 icon = res.getDrawable(R.drawable.ic_google_plus_24dp); 2011 alternateIcon = res.getDrawable(R.drawable.ic_add_to_circles_black_24); 2012 final GPlusOrHangoutsDataItemModel itemModel = 2013 new GPlusOrHangoutsDataItemModel(intent, alternateIntent, 2014 dataItem, secondDataItem, alternateContentDescription, 2015 header, text, context); 2016 2017 populateGPlusOrHangoutsDataItemModel(itemModel); 2018 intent = itemModel.intent; 2019 alternateIntent = itemModel.alternateIntent; 2020 alternateContentDescription = itemModel.alternateContentDescription; 2021 header = itemModel.header; 2022 text = itemModel.text; 2023 } else { 2024 if (GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE.equals( 2025 intent.getDataString())) { 2026 icon = res.getDrawable(R.drawable.ic_add_to_circles_black_24); 2027 } else { 2028 icon = res.getDrawable(R.drawable.ic_google_plus_24dp); 2029 } 2030 } 2031 break; 2032 case MIMETYPE_HANGOUTS: 2033 // If a secondDataItem is available, use it to build an entry with 2034 // alternate actions 2035 if (secondDataItem != null) { 2036 icon = res.getDrawable(R.drawable.ic_hangout_24dp); 2037 alternateIcon = res.getDrawable(R.drawable.ic_hangout_video_24dp); 2038 final GPlusOrHangoutsDataItemModel itemModel = 2039 new GPlusOrHangoutsDataItemModel(intent, alternateIntent, 2040 dataItem, secondDataItem, alternateContentDescription, 2041 header, text, context); 2042 2043 populateGPlusOrHangoutsDataItemModel(itemModel); 2044 intent = itemModel.intent; 2045 alternateIntent = itemModel.alternateIntent; 2046 alternateContentDescription = itemModel.alternateContentDescription; 2047 header = itemModel.header; 2048 text = itemModel.text; 2049 } else { 2050 if (HANGOUTS_DATA_5_VIDEO.equals(intent.getDataString())) { 2051 icon = res.getDrawable(R.drawable.ic_hangout_video_24dp); 2052 } else { 2053 icon = res.getDrawable(R.drawable.ic_hangout_24dp); 2054 } 2055 } 2056 break; 2057 default: 2058 entryContextMenuInfo = new EntryContextMenuInfo(header, mimetype, 2059 dataItem.getMimeType(), dataItem.getId(), 2060 dataItem.isSuperPrimary()); 2061 icon = ResolveCache.getInstance(context).getIcon( 2062 dataItem.getMimeType(), intent); 2063 // Call mutate to create a new Drawable.ConstantState for color filtering 2064 if (icon != null) { 2065 icon.mutate(); 2066 } 2067 shouldApplyColor = false; 2068 } 2069 } 2070 } 2071 2072 if (intent != null) { 2073 // Do not set the intent is there are no resolves 2074 if (!PhoneCapabilityTester.isIntentRegistered(context, intent)) { 2075 intent = null; 2076 } 2077 } 2078 2079 if (alternateIntent != null) { 2080 // Do not set the alternate intent is there are no resolves 2081 if (!PhoneCapabilityTester.isIntentRegistered(context, alternateIntent)) { 2082 alternateIntent = null; 2083 } else if (TextUtils.isEmpty(alternateContentDescription)) { 2084 // Attempt to use package manager to find a suitable content description if needed 2085 alternateContentDescription.append(getIntentResolveLabel(alternateIntent, context)); 2086 } 2087 } 2088 2089 // If the Entry has no visual elements, return null 2090 if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) && 2091 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) { 2092 return null; 2093 } 2094 2095 // Ignore dataIds from the Me profile. 2096 final int dataId = dataItem.getId() > Integer.MAX_VALUE ? 2097 -1 : (int) dataItem.getId(); 2098 2099 return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon, 2100 phoneContentDescription == null 2101 ? new SpannableString(primaryContentDescription.toString()) 2102 : phoneContentDescription, 2103 intent, alternateIcon, alternateIntent, 2104 smsContentDescription == null 2105 ? new SpannableString(alternateContentDescription.toString()) 2106 : smsContentDescription, 2107 shouldApplyColor, isEditable, 2108 entryContextMenuInfo, thirdIcon, thirdIntent, thirdContentDescription, thirdAction, 2109 thirdExtras, iconResourceId); 2110 } 2111 2112 private List<Entry> dataItemsToEntries(List<DataItem> dataItems, 2113 MutableString aboutCardTitleOut) { 2114 // Hangouts and G+ use two data items to create one entry. 2115 if (dataItems.get(0).getMimeType().equals(MIMETYPE_GPLUS_PROFILE) || 2116 dataItems.get(0).getMimeType().equals(MIMETYPE_HANGOUTS)) { 2117 return gPlusOrHangoutsDataItemsToEntries(dataItems); 2118 } else { 2119 final List<Entry> entries = new ArrayList<>(); 2120 for (DataItem dataItem : dataItems) { 2121 final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null, 2122 this, mContactData, aboutCardTitleOut); 2123 if (entry != null) { 2124 entries.add(entry); 2125 } 2126 } 2127 return entries; 2128 } 2129 } 2130 2131 /** 2132 * G+ and Hangout entries are unique in that a single ExpandingEntryCardView.Entry consists 2133 * of two data items. This method attempts to build each entry using the two data items if 2134 * they are available. If there are more or less than two data items, a fall back is used 2135 * and each data item gets its own entry. 2136 */ 2137 private List<Entry> gPlusOrHangoutsDataItemsToEntries(List<DataItem> dataItems) { 2138 final List<Entry> entries = new ArrayList<>(); 2139 final Map<Long, List<DataItem>> buckets = new HashMap<>(); 2140 // Put the data items into buckets based on the raw contact id 2141 for (DataItem dataItem : dataItems) { 2142 List<DataItem> bucket = buckets.get(dataItem.getRawContactId()); 2143 if (bucket == null) { 2144 bucket = new ArrayList<>(); 2145 buckets.put(dataItem.getRawContactId(), bucket); 2146 } 2147 bucket.add(dataItem); 2148 } 2149 2150 // Use the buckets to build entries. If a bucket contains two data items, build the special 2151 // entry, otherwise fall back to the normal entry. 2152 for (List<DataItem> bucket : buckets.values()) { 2153 if (bucket.size() == 2) { 2154 // Use the pair to build an entry 2155 final Entry entry = dataItemToEntry(bucket.get(0), 2156 /* secondDataItem = */ bucket.get(1), this, mContactData, 2157 /* aboutCardName = */ null); 2158 if (entry != null) { 2159 entries.add(entry); 2160 } 2161 } else { 2162 for (DataItem dataItem : bucket) { 2163 final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null, 2164 this, mContactData, /* aboutCardName = */ null); 2165 if (entry != null) { 2166 entries.add(entry); 2167 } 2168 } 2169 } 2170 } 2171 return entries; 2172 } 2173 2174 /** 2175 * Used for statically passing around G+ or Hangouts data items and entry fields to 2176 * populateGPlusOrHangoutsDataItemModel. 2177 */ 2178 private static final class GPlusOrHangoutsDataItemModel { 2179 public Intent intent; 2180 public Intent alternateIntent; 2181 public DataItem dataItem; 2182 public DataItem secondDataItem; 2183 public StringBuilder alternateContentDescription; 2184 public String header; 2185 public String text; 2186 public Context context; 2187 2188 public GPlusOrHangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem, 2189 DataItem secondDataItem, StringBuilder alternateContentDescription, String header, 2190 String text, Context context) { 2191 this.intent = intent; 2192 this.alternateIntent = alternateIntent; 2193 this.dataItem = dataItem; 2194 this.secondDataItem = secondDataItem; 2195 this.alternateContentDescription = alternateContentDescription; 2196 this.header = header; 2197 this.text = text; 2198 this.context = context; 2199 } 2200 } 2201 2202 private static void populateGPlusOrHangoutsDataItemModel( 2203 GPlusOrHangoutsDataItemModel dataModel) { 2204 final Intent secondIntent = new Intent(Intent.ACTION_VIEW); 2205 secondIntent.setDataAndType(ContentUris.withAppendedId(Data.CONTENT_URI, 2206 dataModel.secondDataItem.getId()), dataModel.secondDataItem.getMimeType()); 2207 // There is no guarantee the order the data items come in. Second 2208 // data item does not necessarily mean it's the alternate. 2209 // Hangouts video and Add to circles should be alternate. Swap if needed 2210 if (HANGOUTS_DATA_5_VIDEO.equals( 2211 dataModel.dataItem.getContentValues().getAsString(Data.DATA5)) || 2212 GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE.equals( 2213 dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) { 2214 dataModel.alternateIntent = dataModel.intent; 2215 dataModel.alternateContentDescription = new StringBuilder(dataModel.header); 2216 2217 dataModel.intent = secondIntent; 2218 dataModel.header = dataModel.secondDataItem.buildDataStringForDisplay(dataModel.context, 2219 dataModel.secondDataItem.getDataKind()); 2220 dataModel.text = dataModel.secondDataItem.getDataKind().typeColumn; 2221 } else if (HANGOUTS_DATA_5_MESSAGE.equals( 2222 dataModel.dataItem.getContentValues().getAsString(Data.DATA5)) || 2223 GPLUS_PROFILE_DATA_5_VIEW_PROFILE.equals( 2224 dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) { 2225 dataModel.alternateIntent = secondIntent; 2226 dataModel.alternateContentDescription = new StringBuilder( 2227 dataModel.secondDataItem.buildDataStringForDisplay(dataModel.context, 2228 dataModel.secondDataItem.getDataKind())); 2229 } 2230 } 2231 2232 private static String getIntentResolveLabel(Intent intent, Context context) { 2233 final List<ResolveInfo> matches = context.getPackageManager().queryIntentActivities(intent, 2234 PackageManager.MATCH_DEFAULT_ONLY); 2235 2236 // Pick first match, otherwise best found 2237 ResolveInfo bestResolve = null; 2238 final int size = matches.size(); 2239 if (size == 1) { 2240 bestResolve = matches.get(0); 2241 } else if (size > 1) { 2242 bestResolve = ResolveCache.getInstance(context).getBestResolve(intent, matches); 2243 } 2244 2245 if (bestResolve == null) { 2246 return null; 2247 } 2248 2249 return String.valueOf(bestResolve.loadLabel(context.getPackageManager())); 2250 } 2251 2252 /** 2253 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 2254 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 2255 * on a Nexus 5. 2256 */ 2257 private void extractAndApplyTintFromPhotoViewAsynchronously() { 2258 if (mScroller == null) { 2259 return; 2260 } 2261 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 2262 new AsyncTask<Void, Void, MaterialPalette>() { 2263 @Override 2264 protected MaterialPalette doInBackground(Void... params) { 2265 2266 if (imageViewDrawable instanceof BitmapDrawable && mContactData != null 2267 && mContactData.getThumbnailPhotoBinaryData() != null 2268 && mContactData.getThumbnailPhotoBinaryData().length > 0) { 2269 // Perform the color analysis on the thumbnail instead of the full sized 2270 // image, so that our results will be as similar as possible to the Bugle 2271 // app. 2272 final Bitmap bitmap = BitmapFactory.decodeByteArray( 2273 mContactData.getThumbnailPhotoBinaryData(), 0, 2274 mContactData.getThumbnailPhotoBinaryData().length); 2275 try { 2276 final int primaryColor = colorFromBitmap(bitmap); 2277 if (primaryColor != 0) { 2278 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 2279 primaryColor); 2280 } 2281 } finally { 2282 bitmap.recycle(); 2283 } 2284 } 2285 if (imageViewDrawable instanceof LetterTileDrawable) { 2286 final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor(); 2287 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(primaryColor); 2288 } 2289 return MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(getResources()); 2290 } 2291 2292 @Override 2293 protected void onPostExecute(MaterialPalette palette) { 2294 super.onPostExecute(palette); 2295 if (mHasComputedThemeColor) { 2296 // If we had previously computed a theme color from the contact photo, 2297 // then do not update the theme color. Changing the theme color several 2298 // seconds after QC has started, as a result of an updated/upgraded photo, 2299 // is a jarring experience. On the other hand, changing the theme color after 2300 // a rotation or onNewIntent() is perfectly fine. 2301 return; 2302 } 2303 // Check that the Photo has not changed. If it has changed, the new tint 2304 // color needs to be extracted 2305 if (imageViewDrawable == mPhotoView.getDrawable()) { 2306 mHasComputedThemeColor = true; 2307 setThemeColor(palette); 2308 // update color and photo in suggestion card 2309 onAggregationSuggestionChange(); 2310 } 2311 } 2312 }.execute(); 2313 } 2314 2315 private void setThemeColor(MaterialPalette palette) { 2316 // If the color is invalid, use the predefined default 2317 mColorFilterColor = palette.mPrimaryColor; 2318 mScroller.setHeaderTintColor(mColorFilterColor); 2319 mStatusBarColor = palette.mSecondaryColor; 2320 updateStatusBarColor(); 2321 2322 mColorFilter = 2323 new PorterDuffColorFilter(mColorFilterColor, PorterDuff.Mode.SRC_ATOP); 2324 mContactCard.setColorAndFilter(mColorFilterColor, mColorFilter); 2325 mRecentCard.setColorAndFilter(mColorFilterColor, mColorFilter); 2326 mAboutCard.setColorAndFilter(mColorFilterColor, mColorFilter); 2327 mSuggestionsCancelButton.setTextColor(mColorFilterColor); 2328 } 2329 2330 private void updateStatusBarColor() { 2331 if (mScroller == null || !CompatUtils.isLollipopCompatible()) { 2332 return; 2333 } 2334 final int desiredStatusBarColor; 2335 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 2336 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 2337 desiredStatusBarColor = mStatusBarColor; 2338 } else { 2339 desiredStatusBarColor = Color.TRANSPARENT; 2340 } 2341 // Animate to the new color. 2342 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 2343 getWindow().getStatusBarColor(), desiredStatusBarColor); 2344 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 2345 animation.setEvaluator(new ArgbEvaluator()); 2346 animation.start(); 2347 } 2348 2349 private int colorFromBitmap(Bitmap bitmap) { 2350 // Author of Palette recommends using 24 colors when analyzing profile photos. 2351 final int NUMBER_OF_PALETTE_COLORS = 24; 2352 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 2353 if (palette != null && palette.getVibrantSwatch() != null) { 2354 return palette.getVibrantSwatch().getRgb(); 2355 } 2356 return 0; 2357 } 2358 2359 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 2360 final List<Entry> entries = new ArrayList<>(); 2361 for (ContactInteraction interaction : interactions) { 2362 if (interaction == null) { 2363 continue; 2364 } 2365 entries.add(new Entry(/* id = */ -1, 2366 interaction.getIcon(this), 2367 interaction.getViewHeader(this), 2368 interaction.getViewBody(this), 2369 interaction.getBodyIcon(this), 2370 interaction.getViewFooter(this), 2371 interaction.getFooterIcon(this), 2372 interaction.getContentDescription(this), 2373 interaction.getIntent(), 2374 /* alternateIcon = */ null, 2375 /* alternateIntent = */ null, 2376 /* alternateContentDescription = */ null, 2377 /* shouldApplyColor = */ true, 2378 /* isEditable = */ false, 2379 /* EntryContextMenuInfo = */ null, 2380 /* thirdIcon = */ null, 2381 /* thirdIntent = */ null, 2382 /* thirdContentDescription = */ null, 2383 /* thirdAction = */ Entry.ACTION_NONE, 2384 /* thirdActionExtras = */ null, 2385 interaction.getIconResourceId())); 2386 } 2387 return entries; 2388 } 2389 2390 private final LoaderCallbacks<Contact> mLoaderContactCallbacks = 2391 new LoaderCallbacks<Contact>() { 2392 @Override 2393 public void onLoaderReset(Loader<Contact> loader) { 2394 mContactData = null; 2395 } 2396 2397 @Override 2398 public void onLoadFinished(Loader<Contact> loader, Contact data) { 2399 Trace.beginSection("onLoadFinished()"); 2400 try { 2401 2402 if (isFinishing()) { 2403 return; 2404 } 2405 if (data.isError()) { 2406 // This means either the contact is invalid or we had an 2407 // internal error such as an acore crash. 2408 Log.i(TAG, "Failed to load contact: " + ((ContactLoader)loader).getLookupUri()); 2409 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 2410 Toast.LENGTH_LONG).show(); 2411 finish(); 2412 return; 2413 } 2414 if (data.isNotFound()) { 2415 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 2416 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 2417 Toast.LENGTH_LONG).show(); 2418 finish(); 2419 return; 2420 } 2421 2422 bindContactData(data); 2423 2424 } finally { 2425 Trace.endSection(); 2426 } 2427 } 2428 2429 @Override 2430 public Loader<Contact> onCreateLoader(int id, Bundle args) { 2431 if (mLookupUri == null) { 2432 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 2433 } 2434 // Load all contact data. We need loadGroupMetaData=true to determine whether the 2435 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 2436 return new ContactLoader(getApplicationContext(), mLookupUri, 2437 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 2438 true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 2439 } 2440 }; 2441 2442 @Override 2443 public void onBackPressed() { 2444 if (mScroller != null) { 2445 if (!mIsExitAnimationInProgress) { 2446 mScroller.scrollOffBottom(); 2447 } 2448 } else { 2449 super.onBackPressed(); 2450 } 2451 } 2452 2453 @Override 2454 public void finish() { 2455 super.finish(); 2456 2457 // override transitions to skip the standard window animations 2458 overridePendingTransition(0, 0); 2459 } 2460 2461 private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 2462 new LoaderCallbacks<List<ContactInteraction>>() { 2463 2464 @Override 2465 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 2466 Loader<List<ContactInteraction>> loader = null; 2467 switch (id) { 2468 case LOADER_SMS_ID: 2469 loader = new SmsInteractionsLoader( 2470 QuickContactActivity.this, 2471 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 2472 MAX_SMS_RETRIEVE); 2473 break; 2474 case LOADER_CALENDAR_ID: 2475 final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS); 2476 List<String> emailsList = null; 2477 if (emailsArray != null) { 2478 emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)); 2479 } 2480 loader = new CalendarInteractionsLoader( 2481 QuickContactActivity.this, 2482 emailsList, 2483 MAX_FUTURE_CALENDAR_RETRIEVE, 2484 MAX_PAST_CALENDAR_RETRIEVE, 2485 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 2486 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 2487 break; 2488 case LOADER_CALL_LOG_ID: 2489 loader = new CallLogInteractionsLoader( 2490 QuickContactActivity.this, 2491 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 2492 MAX_CALL_LOG_RETRIEVE); 2493 } 2494 return loader; 2495 } 2496 2497 @Override 2498 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 2499 List<ContactInteraction> data) { 2500 mRecentLoaderResults.put(loader.getId(), data); 2501 2502 if (isAllRecentDataLoaded()) { 2503 bindRecentData(); 2504 } 2505 } 2506 2507 @Override 2508 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 2509 mRecentLoaderResults.remove(loader.getId()); 2510 } 2511 }; 2512 2513 private boolean isAllRecentDataLoaded() { 2514 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 2515 } 2516 2517 private void bindRecentData() { 2518 final List<ContactInteraction> allInteractions = new ArrayList<>(); 2519 final List<List<Entry>> interactionsWrapper = new ArrayList<>(); 2520 2521 // Serialize mRecentLoaderResults into a single list. This should be done on the main 2522 // thread to avoid races against mRecentLoaderResults edits. 2523 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 2524 allInteractions.addAll(loaderInteractions); 2525 } 2526 2527 mRecentDataTask = new AsyncTask<Void, Void, Void>() { 2528 @Override 2529 protected Void doInBackground(Void... params) { 2530 Trace.beginSection("sort recent loader results"); 2531 2532 // Sort the interactions by most recent 2533 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 2534 @Override 2535 public int compare(ContactInteraction a, ContactInteraction b) { 2536 if (a == null && b == null) { 2537 return 0; 2538 } 2539 if (a == null) { 2540 return 1; 2541 } 2542 if (b == null) { 2543 return -1; 2544 } 2545 if (a.getInteractionDate() > b.getInteractionDate()) { 2546 return -1; 2547 } 2548 if (a.getInteractionDate() == b.getInteractionDate()) { 2549 return 0; 2550 } 2551 return 1; 2552 } 2553 }); 2554 2555 Trace.endSection(); 2556 Trace.beginSection("contactInteractionsToEntries"); 2557 2558 // Wrap each interaction in its own list so that an icon is displayed for each entry 2559 for (Entry contactInteraction : contactInteractionsToEntries(allInteractions)) { 2560 List<Entry> entryListWrapper = new ArrayList<>(1); 2561 entryListWrapper.add(contactInteraction); 2562 interactionsWrapper.add(entryListWrapper); 2563 } 2564 2565 Trace.endSection(); 2566 return null; 2567 } 2568 2569 @Override 2570 protected void onPostExecute(Void aVoid) { 2571 super.onPostExecute(aVoid); 2572 Trace.beginSection("initialize recents card"); 2573 2574 if (allInteractions.size() > 0) { 2575 mRecentCard.initialize(interactionsWrapper, 2576 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 2577 /* isExpanded = */ mRecentCard.isExpanded(), /* isAlwaysExpanded = */ false, 2578 mExpandingEntryCardViewListener, mScroller); 2579 mRecentCard.setVisibility(View.VISIBLE); 2580 } 2581 2582 Trace.endSection(); 2583 2584 // About card is initialized along with the contact card, but since it appears after 2585 // the recent card in the UI, we hold off until making it visible until the recent 2586 // card is also ready to avoid stuttering. 2587 if (mAboutCard.shouldShow()) { 2588 mAboutCard.setVisibility(View.VISIBLE); 2589 } else { 2590 mAboutCard.setVisibility(View.GONE); 2591 } 2592 mRecentDataTask = null; 2593 } 2594 }; 2595 mRecentDataTask.execute(); 2596 } 2597 2598 @Override 2599 protected void onStop() { 2600 super.onStop(); 2601 2602 if (mEntriesAndActionsTask != null) { 2603 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 2604 // results on the UI thread. In some circumstances Activities are killed without 2605 // onStop() being called. This is not a problem, because in these circumstances 2606 // the entire process will be killed. 2607 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 2608 } 2609 if (mRecentDataTask != null) { 2610 mRecentDataTask.cancel(/* mayInterruptIfRunning = */ false); 2611 } 2612 } 2613 2614 @Override 2615 public void onDestroy() { 2616 super.onDestroy(); 2617 if (mAggregationSuggestionEngine != null) { 2618 mAggregationSuggestionEngine.quit(); 2619 } 2620 } 2621 2622 /** 2623 * Returns true if it is possible to edit the current contact. 2624 */ 2625 private boolean isContactEditable() { 2626 return mContactData != null && !mContactData.isDirectoryEntry(); 2627 } 2628 2629 /** 2630 * Returns true if it is possible to share the current contact. 2631 */ 2632 private boolean isContactShareable() { 2633 return mContactData != null && !mContactData.isDirectoryEntry(); 2634 } 2635 2636 private Intent getEditContactIntent() { 2637 return EditorIntents.createCompactEditContactIntent( 2638 mContactData.getLookupUri(), 2639 mHasComputedThemeColor 2640 ? new MaterialPalette(mColorFilterColor, mStatusBarColor) : null, 2641 mContactData.getPhotoId()); 2642 } 2643 2644 private void editContact() { 2645 mHasIntentLaunched = true; 2646 mContactLoader.cacheResult(); 2647 startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 2648 } 2649 2650 private void deleteContact() { 2651 final Uri contactUri = mContactData.getLookupUri(); 2652 ContactDeletionInteraction.start(this, contactUri, /* finishActivityWhenDone =*/ true); 2653 } 2654 2655 private void toggleStar(MenuItem starredMenuItem) { 2656 // Make sure there is a contact 2657 if (mContactData != null) { 2658 // Read the current starred value from the UI instead of using the last 2659 // loaded state. This allows rapid tapping without writing the same 2660 // value several times 2661 final boolean isStarred = starredMenuItem.isChecked(); 2662 2663 // To improve responsiveness, swap out the picture (and tag) in the UI already 2664 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem, 2665 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 2666 !isStarred); 2667 2668 // Now perform the real save 2669 final Intent intent = ContactSaveService.createSetStarredIntent( 2670 QuickContactActivity.this, mContactData.getLookupUri(), !isStarred); 2671 startService(intent); 2672 2673 final CharSequence accessibilityText = !isStarred 2674 ? getResources().getText(R.string.description_action_menu_add_star) 2675 : getResources().getText(R.string.description_action_menu_remove_star); 2676 // Accessibility actions need to have an associated view. We can't access the MenuItem's 2677 // underlying view, so put this accessibility action on the root view. 2678 mScroller.announceForAccessibility(accessibilityText); 2679 } 2680 } 2681 2682 private void shareContact() { 2683 final String lookupKey = mContactData.getLookupKey(); 2684 final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 2685 final Intent intent = new Intent(Intent.ACTION_SEND); 2686 intent.setType(Contacts.CONTENT_VCARD_TYPE); 2687 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 2688 2689 // Launch chooser to share contact via 2690 final CharSequence chooseTitle = getText(R.string.share_via); 2691 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 2692 2693 try { 2694 mHasIntentLaunched = true; 2695 ImplicitIntentsUtil.startActivityOutsideApp(this, chooseIntent); 2696 } catch (final ActivityNotFoundException ex) { 2697 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 2698 } 2699 } 2700 2701 /** 2702 * Creates a launcher shortcut with the current contact. 2703 */ 2704 private void createLauncherShortcutWithContact() { 2705 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 2706 new OnShortcutIntentCreatedListener() { 2707 2708 @Override 2709 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 2710 // Broadcast the shortcutIntent to the launcher to create a 2711 // shortcut to this contact 2712 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 2713 QuickContactActivity.this.sendBroadcast(shortcutIntent); 2714 2715 // Send a toast to give feedback to the user that a shortcut to this 2716 // contact was added to the launcher. 2717 final String displayName = mContactData.getDisplayName(); 2718 final String toastMessage = TextUtils.isEmpty(displayName) 2719 ? getString(R.string.createContactShortcutSuccessful_NoName) 2720 : getString(R.string.createContactShortcutSuccessful, displayName); 2721 Toast.makeText(QuickContactActivity.this, toastMessage, 2722 Toast.LENGTH_SHORT).show(); 2723 } 2724 2725 }); 2726 builder.createContactShortcutIntent(mContactData.getLookupUri()); 2727 } 2728 2729 private boolean isShortcutCreatable() { 2730 if (mContactData == null || mContactData.isUserProfile() || 2731 mContactData.isDirectoryEntry()) { 2732 return false; 2733 } 2734 final Intent createShortcutIntent = new Intent(); 2735 createShortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 2736 final List<ResolveInfo> receivers = getPackageManager() 2737 .queryBroadcastReceivers(createShortcutIntent, 0); 2738 return receivers != null && receivers.size() > 0; 2739 } 2740 2741 @Override 2742 public boolean onCreateOptionsMenu(Menu menu) { 2743 final MenuInflater inflater = getMenuInflater(); 2744 inflater.inflate(R.menu.quickcontact, menu); 2745 return true; 2746 } 2747 2748 @Override 2749 public boolean onPrepareOptionsMenu(Menu menu) { 2750 if (mContactData != null) { 2751 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 2752 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem, 2753 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 2754 mContactData.getStarred()); 2755 2756 // Configure edit MenuItem 2757 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 2758 editMenuItem.setVisible(true); 2759 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 2760 .isInvisibleAndAddable(mContactData, this)) { 2761 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp); 2762 editMenuItem.setTitle(R.string.menu_add_contact); 2763 } else if (isContactEditable()) { 2764 editMenuItem.setIcon(R.drawable.ic_create_24dp); 2765 editMenuItem.setTitle(R.string.menu_editContact); 2766 } else { 2767 editMenuItem.setVisible(false); 2768 } 2769 2770 final MenuItem deleteMenuItem = menu.findItem(R.id.menu_delete); 2771 deleteMenuItem.setVisible(isContactEditable() && !mContactData.isUserProfile()); 2772 2773 final MenuItem shareMenuItem = menu.findItem(R.id.menu_share); 2774 shareMenuItem.setVisible(isContactShareable()); 2775 2776 final MenuItem shortcutMenuItem = menu.findItem(R.id.menu_create_contact_shortcut); 2777 shortcutMenuItem.setVisible(isShortcutCreatable()); 2778 2779 final MenuItem helpMenu = menu.findItem(R.id.menu_help); 2780 helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable()); 2781 2782 return true; 2783 } 2784 return false; 2785 } 2786 2787 @Override 2788 public boolean onOptionsItemSelected(MenuItem item) { 2789 switch (item.getItemId()) { 2790 case R.id.menu_star: 2791 toggleStar(item); 2792 return true; 2793 case R.id.menu_edit: 2794 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 2795 // This action is used to launch the contact selector, with the option of 2796 // creating a new contact. Creating a new contact is an INSERT, while selecting 2797 // an exisiting one is an edit. The fields in the edit screen will be 2798 // prepopulated with data. 2799 2800 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 2801 intent.setType(Contacts.CONTENT_ITEM_TYPE); 2802 2803 ArrayList<ContentValues> values = mContactData.getContentValues(); 2804 2805 // Only pre-fill the name field if the provided display name is an nickname 2806 // or better (e.g. structured name, nickname) 2807 if (mContactData.getDisplayNameSource() >= DisplayNameSources.NICKNAME) { 2808 intent.putExtra(Intents.Insert.NAME, mContactData.getDisplayName()); 2809 } else if (mContactData.getDisplayNameSource() 2810 == DisplayNameSources.ORGANIZATION) { 2811 // This is probably an organization. Instead of copying the organization 2812 // name into a name entry, copy it into the organization entry. This 2813 // way we will still consider the contact an organization. 2814 final ContentValues organization = new ContentValues(); 2815 organization.put(Organization.COMPANY, mContactData.getDisplayName()); 2816 organization.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); 2817 values.add(organization); 2818 } 2819 2820 // Last time used and times used are aggregated values from the usage stat 2821 // table. They need to be removed from data values so the SQL table can insert 2822 // properly 2823 for (ContentValues value : values) { 2824 value.remove(Data.LAST_TIME_USED); 2825 value.remove(Data.TIMES_USED); 2826 } 2827 intent.putExtra(Intents.Insert.DATA, values); 2828 2829 // If the contact can only export to the same account, add it to the intent. 2830 // Otherwise the ContactEditorFragment will show a dialog for selecting an 2831 // account. 2832 if (mContactData.getDirectoryExportSupport() == 2833 Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY) { 2834 intent.putExtra(Intents.Insert.EXTRA_ACCOUNT, 2835 new Account(mContactData.getDirectoryAccountName(), 2836 mContactData.getDirectoryAccountType())); 2837 intent.putExtra(Intents.Insert.EXTRA_DATA_SET, 2838 mContactData.getRawContacts().get(0).getDataSet()); 2839 } 2840 2841 // Add this flag to disable the delete menu option on directory contact joins 2842 // with local contacts. The delete option is ambiguous when joining contacts. 2843 intent.putExtra(ContactEditorFragment.INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION, 2844 true); 2845 2846 startActivityForResult(intent, REQUEST_CODE_CONTACT_SELECTION_ACTIVITY); 2847 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 2848 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 2849 } else if (isContactEditable()) { 2850 editContact(); 2851 } 2852 return true; 2853 case R.id.menu_delete: 2854 if (isContactEditable()) { 2855 deleteContact(); 2856 } 2857 return true; 2858 case R.id.menu_share: 2859 if (isContactShareable()) { 2860 shareContact(); 2861 } 2862 return true; 2863 case R.id.menu_create_contact_shortcut: 2864 if (isShortcutCreatable()) { 2865 createLauncherShortcutWithContact(); 2866 } 2867 return true; 2868 case R.id.menu_help: 2869 HelpUtils.launchHelpAndFeedbackForContactScreen(this); 2870 return true; 2871 default: 2872 return super.onOptionsItemSelected(item); 2873 } 2874 } 2875} 2876