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