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