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