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