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 onAggregationSuggestionChange()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 collapseSuggestionList()557 private void collapseSuggestionList() { 558 mCollapsedSuggestionCardView.setVisibility(View.VISIBLE); 559 mExpandSuggestionCardView.setVisibility(View.GONE); 560 mIsSuggestionListCollapsed = true; 561 } 562 expandSuggestionList()563 private void expandSuggestionList() { 564 mCollapsedSuggestionCardView.setVisibility(View.GONE); 565 mExpandSuggestionCardView.setVisibility(View.VISIBLE); 566 mIsSuggestionListCollapsed = false; 567 } 568 inflateSuggestionListView(final Suggestion suggestion)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 enableLinkButton()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 onContactsJoined()662 public void onContactsJoined() { 663 disableLinkButton(); 664 } 665 disableLinkButton()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 onContextItemSelected(MenuItem item)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 SelectAccountDialogFragmentListener()760 public SelectAccountDialogFragmentListener() {} 761 762 @Override onAccountChosen(AccountWithDataSet account, Bundle extraArgs)763 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 764 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 765 account, mQuickContactActivity); 766 } 767 768 @Override onAccountSelectorCancelled()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 */ setQuickContactActivity(QuickContactActivity quickContactActivity)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 dispatchTouchEvent(MotionEvent ev)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 onCreate(Bundle savedInstanceState)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 onActivityResult(int requestCode, int resultCode, Intent data)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 onNewIntent(Intent intent)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 onSaveInstanceState(Bundle savedInstanceState)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 processIntent(Intent intent)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 destroyInteractionLoaders()1189 private void destroyInteractionLoaders() { 1190 for (int interactionLoaderId : mRecentLoaderIds) { 1191 getLoaderManager().destroyLoader(interactionLoaderId); 1192 } 1193 } 1194 runEntranceAnimation()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 isMultiWindowOnPhone()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. */ setHeaderNameText(int resId)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. */ setHeaderNameText(String value, boolean isPhoneNumber)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 */ isMimeExcluded(String mimeType)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 */ bindContactData(final Contact data)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 bindDataToCards(Cp2DataCardModel cp2DataCardModel)1291 private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) { 1292 startInteractionLoaders(cp2DataCardModel); 1293 populateContactAndAboutCard(cp2DataCardModel, /* shouldAddPhoneticName */ true); 1294 populateSuggestionCard(); 1295 } 1296 startInteractionLoaders(Cp2DataCardModel cp2DataCardModel)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 showActivity()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 buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap)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 onResume()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 populateSuggestionCard()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 populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel, boolean shouldAddPhoneticName)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 */ initializeNoContactDetailCard()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 */ generateDataModelFromContact( Contact data)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 */ dataItemToEntry(DataItem dataItem, DataItem secondDataItem, Context context, Contact contactData, final MutableString aboutCardName)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 dataItemsToEntries(List<DataItem> dataItems, MutableString aboutCardTitleOut)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 */ gPlusOrHangoutsDataItemsToEntries(List<DataItem> dataItems)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 GPlusOrHangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem, DataItem secondDataItem, StringBuilder alternateContentDescription, String header, String text, Context context)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 populateGPlusOrHangoutsDataItemModel( GPlusOrHangoutsDataItemModel dataModel)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 getIntentResolveLabel(Intent intent, Context context)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 */ extractAndApplyTintFromPhotoViewAsynchronously()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 setThemeColor(MaterialPalette palette)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 updateStatusBarColor()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 colorFromBitmap(Bitmap bitmap)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 contactInteractionsToEntries(List<ContactInteraction> interactions)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 onBackPressed()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 finish()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 isAllRecentDataLoaded()2531 private boolean isAllRecentDataLoaded() { 2532 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 2533 } 2534 bindRecentData()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 onStop()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 onDestroy()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 */ isContactEditable()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 */ isContactShareable()2650 private boolean isContactShareable() { 2651 return mContactData != null && !mContactData.isDirectoryEntry(); 2652 } 2653 getEditContactIntent()2654 private Intent getEditContactIntent() { 2655 return EditorIntents.createCompactEditContactIntent( 2656 mContactData.getLookupUri(), 2657 mHasComputedThemeColor 2658 ? new MaterialPalette(mColorFilterColor, mStatusBarColor) : null, 2659 mContactData.getPhotoId()); 2660 } 2661 editContact()2662 private void editContact() { 2663 mHasIntentLaunched = true; 2664 mContactLoader.cacheResult(); 2665 startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 2666 } 2667 deleteContact()2668 private void deleteContact() { 2669 final Uri contactUri = mContactData.getLookupUri(); 2670 ContactDeletionInteraction.start(this, contactUri, /* finishActivityWhenDone =*/ true); 2671 } 2672 toggleStar(MenuItem starredMenuItem)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 shareContact()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 */ createLauncherShortcutWithContact()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 isShortcutCreatable()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 onCreateOptionsMenu(Menu menu)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 onPrepareOptionsMenu(Menu menu)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 onOptionsItemSelected(MenuItem item)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