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