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