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