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