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