QuickContactActivity.java revision dd7419d90cda5c52e81491bea3cf3c3f4e0535c7
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 17package com.android.contacts.quickcontact; 18 19import android.animation.Animator; 20import android.animation.Animator.AnimatorListener; 21import android.animation.AnimatorListenerAdapter; 22import android.animation.ArgbEvaluator; 23import android.animation.ObjectAnimator; 24import android.app.Activity; 25import android.app.Fragment; 26import android.app.LoaderManager.LoaderCallbacks; 27import android.app.SearchManager; 28import android.content.ActivityNotFoundException; 29import android.content.ComponentName; 30import android.content.ContentUris; 31import android.content.ContentValues; 32import android.content.Intent; 33import android.content.Loader; 34import android.content.pm.PackageManager; 35import android.content.pm.ResolveInfo; 36import android.graphics.Bitmap; 37import android.graphics.Color; 38import android.graphics.PorterDuff; 39import android.graphics.PorterDuffColorFilter; 40import android.graphics.drawable.BitmapDrawable; 41import android.graphics.drawable.ColorDrawable; 42import android.graphics.drawable.Drawable; 43import android.net.ParseException; 44import android.net.Uri; 45import android.net.WebAddress; 46import android.os.AsyncTask; 47import android.os.Bundle; 48import android.os.Trace; 49import android.provider.CalendarContract; 50import android.provider.ContactsContract; 51import android.provider.ContactsContract.CommonDataKinds.Email; 52import android.provider.ContactsContract.CommonDataKinds.Event; 53import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 54import android.provider.ContactsContract.CommonDataKinds.Identity; 55import android.provider.ContactsContract.CommonDataKinds.Im; 56import android.provider.ContactsContract.CommonDataKinds.Nickname; 57import android.provider.ContactsContract.CommonDataKinds.Note; 58import android.provider.ContactsContract.CommonDataKinds.Organization; 59import android.provider.ContactsContract.CommonDataKinds.Phone; 60import android.provider.ContactsContract.CommonDataKinds.Relation; 61import android.provider.ContactsContract.CommonDataKinds.SipAddress; 62import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 63import android.provider.ContactsContract.CommonDataKinds.Website; 64import android.provider.ContactsContract.Contacts; 65import android.provider.ContactsContract.Data; 66import android.provider.ContactsContract.DisplayNameSources; 67import android.provider.ContactsContract.DataUsageFeedback; 68import android.provider.ContactsContract.QuickContact; 69import android.provider.ContactsContract.RawContacts; 70import android.support.v7.graphics.Palette; 71import android.text.TextUtils; 72import android.util.Log; 73import android.util.Pair; 74import android.view.Menu; 75import android.view.MenuInflater; 76import android.view.MenuItem; 77import android.view.View; 78import android.view.View.OnClickListener; 79import android.view.WindowManager; 80import android.widget.ImageView; 81import android.widget.Toast; 82import android.widget.Toolbar; 83 84import com.android.contacts.ContactSaveService; 85import com.android.contacts.ContactsActivity; 86import com.android.contacts.NfcHandler; 87import com.android.contacts.R; 88import com.android.contacts.common.CallUtil; 89import com.android.contacts.common.Collapser; 90import com.android.contacts.common.ContactsUtils; 91import com.android.contacts.common.editor.SelectAccountDialogFragment; 92import com.android.contacts.common.lettertiles.LetterTileDrawable; 93import com.android.contacts.common.list.ShortcutIntentBuilder; 94import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 95import com.android.contacts.common.model.AccountTypeManager; 96import com.android.contacts.common.model.Contact; 97import com.android.contacts.common.model.ContactLoader; 98import com.android.contacts.common.model.RawContact; 99import com.android.contacts.common.model.account.AccountType; 100import com.android.contacts.common.model.account.AccountWithDataSet; 101import com.android.contacts.common.model.dataitem.DataItem; 102import com.android.contacts.common.model.dataitem.DataKind; 103import com.android.contacts.common.model.dataitem.EmailDataItem; 104import com.android.contacts.common.model.dataitem.EventDataItem; 105import com.android.contacts.common.model.dataitem.ImDataItem; 106import com.android.contacts.common.model.dataitem.NicknameDataItem; 107import com.android.contacts.common.model.dataitem.NoteDataItem; 108import com.android.contacts.common.model.dataitem.OrganizationDataItem; 109import com.android.contacts.common.model.dataitem.PhoneDataItem; 110import com.android.contacts.common.model.dataitem.RelationDataItem; 111import com.android.contacts.common.model.dataitem.SipAddressDataItem; 112import com.android.contacts.common.model.dataitem.StructuredNameDataItem; 113import com.android.contacts.common.model.dataitem.StructuredPostalDataItem; 114import com.android.contacts.common.model.dataitem.WebsiteDataItem; 115import com.android.contacts.common.util.DateUtils; 116import com.android.contacts.common.util.MaterialColorMapUtils; 117import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 118import com.android.contacts.detail.ContactDetailDisplayUtils; 119import com.android.contacts.interactions.CalendarInteractionsLoader; 120import com.android.contacts.interactions.CallLogInteractionsLoader; 121import com.android.contacts.interactions.ContactDeletionInteraction; 122import com.android.contacts.interactions.ContactInteraction; 123import com.android.contacts.interactions.SmsInteractionsLoader; 124import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 125import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener; 126import com.android.contacts.util.ImageViewDrawableSetter; 127import com.android.contacts.util.PhoneCapabilityTester; 128import com.android.contacts.util.SchedulingUtils; 129import com.android.contacts.util.StructuredPostalUtils; 130import com.android.contacts.widget.MultiShrinkScroller; 131import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 132import com.google.common.base.Preconditions; 133import com.google.common.collect.Lists; 134 135import java.util.ArrayList; 136import java.util.Arrays; 137import java.util.Calendar; 138import java.util.Collections; 139import java.util.Comparator; 140import java.util.Date; 141import java.util.HashMap; 142import java.util.List; 143import java.util.Map; 144 145/** 146 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 147 * data asynchronously, and then shows a popup with details centered around 148 * {@link Intent#getSourceBounds()}. 149 */ 150public class QuickContactActivity extends ContactsActivity { 151 152 /** 153 * QuickContacts immediately takes up the full screen. All possible information is shown. 154 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 155 * should only be used by the Contacts app. 156 */ 157 public static final int MODE_FULLY_EXPANDED = 4; 158 159 private static final String TAG = "QuickContact"; 160 161 private static final String KEY_THEME_COLOR = "theme_color"; 162 163 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150; 164 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 165 private static final int SCRIM_COLOR = Color.argb(0xB2, 0, 0, 0); 166 private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms"; 167 168 /** This is the Intent action to install a shortcut in the launcher. */ 169 private static final String ACTION_INSTALL_SHORTCUT = 170 "com.android.launcher.action.INSTALL_SHORTCUT"; 171 172 @SuppressWarnings("deprecation") 173 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 174 175 private static final String MIMETYPE_GPLUS_PROFILE = 176 "vnd.android.cursor.item/vnd.googleplus.profile"; 177 private static final String INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE = "Add to circle"; 178 private static final String MIMETYPE_HANGOUTS = 179 "vnd.android.cursor.item/vnd.googleplus.profile.comm"; 180 private static final String INTENT_DATA_HANGOUTS_VIDEO = "Start video call"; 181 182 private Uri mLookupUri; 183 private String[] mExcludeMimes; 184 private int mExtraMode; 185 private int mStatusBarColor; 186 private boolean mHasAlreadyBeenOpened; 187 188 private ImageView mPhotoView; 189 private View mTransparentView; 190 private ExpandingEntryCardView mContactCard; 191 private ExpandingEntryCardView mNoContactDetailsCard; 192 private ExpandingEntryCardView mRecentCard; 193 private ExpandingEntryCardView mAboutCard; 194 /** 195 * This list contains all the {@link DataItem}s. Each nested list contains all data items of a 196 * specific mimetype in sorted order, using mWithinMimeTypeDataItemComparator. The mimetype 197 * lists are sorted using mAmongstMimeTypeDataItemComparator. 198 */ 199 private List<List<DataItem>> mDataItemsList; 200 /** 201 * A map between a mimetype string and the corresponding list of data items. The data items 202 * are in sorted order using mWithinMimeTypeDataItemComparator. 203 */ 204 private Map<String, List<DataItem>> mDataItemsMap; 205 private MultiShrinkScroller mScroller; 206 private SelectAccountDialogFragmentListener mSelectAccountFragmentListener; 207 private AsyncTask<Void, Void, Pair<List<List<DataItem>>, Map<String, List<DataItem>>>> 208 mEntriesAndActionsTask; 209 private ColorDrawable mWindowScrim; 210 private boolean mIsWaitingForOtherPieceOfExitAnimation; 211 private boolean mIsExitAnimationInProgress; 212 private boolean mHasComputedThemeColor; 213 214 private Contact mContactData; 215 private ContactLoader mContactLoader; 216 private PorterDuffColorFilter mColorFilter; 217 218 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 219 220 /** 221 * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. 222 * 223 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 224 * in the order specified here.</p> 225 * 226 * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order 227 * specified here.</p> 228 * 229 * <p>The rest go between them, in the order in the array.</p> 230 */ 231 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 232 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); 233 234 /** See {@link #LEADING_MIMETYPES}. */ 235 private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( 236 StructuredPostal.CONTENT_ITEM_TYPE); 237 238 private static final List<String> ABOUT_CARD_MIMETYPES = Lists.newArrayList( 239 Event.CONTENT_ITEM_TYPE, GroupMembership.CONTENT_ITEM_TYPE, Identity.CONTENT_ITEM_TYPE, 240 Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE, 241 Organization.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); 242 243 /** Id for the background contact loader */ 244 private static final int LOADER_CONTACT_ID = 0; 245 246 private static final String KEY_LOADER_EXTRA_PHONES = 247 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES"; 248 249 /** Id for the background Sms Loader */ 250 private static final int LOADER_SMS_ID = 1; 251 private static final int MAX_SMS_RETRIEVE = 3; 252 253 /** Id for the back Calendar Loader */ 254 private static final int LOADER_CALENDAR_ID = 2; 255 private static final String KEY_LOADER_EXTRA_EMAILS = 256 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS"; 257 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 258 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 259 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 260 180L * 24L * 60L * 60L * 1000L /* 180 days */; 261 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 262 36L * 60L * 60L * 1000L /* 36 hours */; 263 264 /** Id for the background Call Log Loader */ 265 private static final int LOADER_CALL_LOG_ID = 3; 266 private static final int MAX_CALL_LOG_RETRIEVE = 3; 267 private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3; 268 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 269 private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2; 270 271 272 private static final int[] mRecentLoaderIds = new int[]{ 273 LOADER_SMS_ID, 274 LOADER_CALENDAR_ID, 275 LOADER_CALL_LOG_ID}; 276 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults = new HashMap<>(); 277 278 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 279 280 final OnClickListener mEntryClickHandler = new OnClickListener() { 281 @Override 282 public void onClick(View v) { 283 // Data Id is stored as the entry view id 284 final int dataId = v.getId(); 285 if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) { 286 editContact(); 287 return; 288 } 289 final Object intentObject = v.getTag(); 290 if (intentObject == null || !(intentObject instanceof Intent)) { 291 Log.w(TAG, "Intent tag was not used correctly"); 292 return; 293 } 294 final Intent intent = (Intent) intentObject; 295 296 // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id 297 // so the exact usage type is not necessary in all cases 298 String usageType = DataUsageFeedback.USAGE_TYPE_CALL; 299 300 final String scheme = intent.getData().getScheme(); 301 if ((scheme != null && scheme.equals(CallUtil.SCHEME_SMSTO)) || 302 (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) { 303 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT; 304 } 305 306 // Data IDs start at 1 so anything less is invalid 307 if (dataId > 0) { 308 final Uri uri = DataUsageFeedback.FEEDBACK_URI.buildUpon() 309 .appendPath(String.valueOf(dataId)) 310 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType) 311 .build(); 312 final boolean successful = getContentResolver().update( 313 uri, new ContentValues(), null, null) > 0; 314 if (!successful) { 315 Log.w(TAG, "DataUsageFeedback increment failed"); 316 } 317 } else { 318 Log.w(TAG, "Invalid Data ID"); 319 } 320 321 startActivity(intent); 322 } 323 }; 324 325 final ExpandingEntryCardViewListener mExpandingEntryCardViewListener 326 = new ExpandingEntryCardViewListener() { 327 @Override 328 public void onCollapse(int heightDelta) { 329 mScroller.prepareForShrinkingScrollChild(heightDelta); 330 } 331 }; 332 333 /** 334 * Headless fragment used to handle account selection callbacks invoked from 335 * {@link DirectoryContactUtil}. 336 */ 337 public static class SelectAccountDialogFragmentListener extends Fragment 338 implements SelectAccountDialogFragment.Listener { 339 340 private QuickContactActivity mQuickContactActivity; 341 342 public SelectAccountDialogFragmentListener() {} 343 344 @Override 345 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 346 DirectoryContactUtil.createCopy(mQuickContactActivity.mContactData.getContentValues(), 347 account, mQuickContactActivity); 348 } 349 350 @Override 351 public void onAccountSelectorCancelled() {} 352 353 /** 354 * Set the parent activity. Since rotation can cause this fragment to be used across 355 * more than one activity instance, we need to explicitly set this value instead 356 * of making this class non-static. 357 */ 358 public void setQuickContactActivity(QuickContactActivity quickContactActivity) { 359 mQuickContactActivity = quickContactActivity; 360 } 361 } 362 363 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 364 = new MultiShrinkScrollerListener() { 365 @Override 366 public void onScrolledOffBottom() { 367 if (!mIsWaitingForOtherPieceOfExitAnimation) { 368 finish(); 369 return; 370 } 371 mIsWaitingForOtherPieceOfExitAnimation = false; 372 } 373 374 @Override 375 public void onEnterFullscreen() { 376 updateStatusBarColor(); 377 } 378 379 @Override 380 public void onExitFullscreen() { 381 updateStatusBarColor(); 382 } 383 384 @Override 385 public void onStartScrollOffBottom() { 386 // Remove the window shim now that we are starting an Activity exit animation. 387 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 388 final ObjectAnimator animator = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0xFF, 0); 389 animator.addListener(mExitWindowShimAnimationListener); 390 animator.setDuration(duration).start(); 391 mIsWaitingForOtherPieceOfExitAnimation = true; 392 mIsExitAnimationInProgress = true; 393 } 394 }; 395 396 final AnimatorListener mExitWindowShimAnimationListener = new AnimatorListenerAdapter() { 397 @Override 398 public void onAnimationEnd(Animator animation) { 399 if (!mIsWaitingForOtherPieceOfExitAnimation) { 400 finish(); 401 return; 402 } 403 mIsWaitingForOtherPieceOfExitAnimation = false; 404 } 405 }; 406 407 408 /** 409 * Data items are compared to the same mimetype based off of three qualities: 410 * 1. Super primary 411 * 2. Primary 412 * 3. Times used 413 */ 414 private final Comparator<DataItem> mWithinMimeTypeDataItemComparator = 415 new Comparator<DataItem>() { 416 @Override 417 public int compare(DataItem lhs, DataItem rhs) { 418 if (!lhs.getMimeType().equals(rhs.getMimeType())) { 419 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " + 420 lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType()); 421 return 0; 422 } 423 424 if (lhs.isSuperPrimary()) { 425 return -1; 426 } else if (rhs.isSuperPrimary()) { 427 return 1; 428 } else if (lhs.isPrimary() && !rhs.isPrimary()) { 429 return -1; 430 } else if (!lhs.isPrimary() && rhs.isPrimary()) { 431 return 1; 432 } else { 433 final int lhsTimesUsed = 434 lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 435 final int rhsTimesUsed = 436 rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 437 438 return rhsTimesUsed - lhsTimesUsed; 439 } 440 } 441 }; 442 443 private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator = 444 new Comparator<List<DataItem>> () { 445 @Override 446 public int compare(List<DataItem> lhsList, List<DataItem> rhsList) { 447 DataItem lhs = lhsList.get(0); 448 DataItem rhs = rhsList.get(0); 449 final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 450 final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 451 final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed; 452 if (timesUsedDifference != 0) { 453 return timesUsedDifference; 454 } 455 456 final long lhsLastTimeUsed = 457 lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed(); 458 final long rhsLastTimeUsed = 459 rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed(); 460 final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed; 461 if (lastTimeUsedDifference > 0) { 462 return 1; 463 } else if (lastTimeUsedDifference < 0) { 464 return -1; 465 } 466 467 // Times used and last time used are the same. Resort to statically defined. 468 final String lhsMimeType = lhs.getMimeType(); 469 final String rhsMimeType = rhs.getMimeType(); 470 for (String mimeType : LEADING_MIMETYPES) { 471 if (lhsMimeType.equals(mimeType)) { 472 return -1; 473 } else if (rhsMimeType.equals(mimeType)) { 474 return 1; 475 } 476 } 477 // Trailing types come last, so flip the returns 478 for (String mimeType : TRAILING_MIMETYPES) { 479 if (lhsMimeType.equals(mimeType)) { 480 return 1; 481 } else if (rhsMimeType.equals(mimeType)) { 482 return -1; 483 } 484 } 485 return 0; 486 } 487 }; 488 489 @Override 490 protected void onCreate(Bundle savedInstanceState) { 491 Trace.beginSection("onCreate()"); 492 super.onCreate(savedInstanceState); 493 494 getWindow().setStatusBarColor(Color.TRANSPARENT); 495 496 processIntent(getIntent()); 497 498 // Show QuickContact in front of soft input 499 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 500 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 501 502 setContentView(R.layout.quickcontact_activity); 503 504 mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 505 mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card); 506 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 507 mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card); 508 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 509 510 mNoContactDetailsCard.setOnClickListener(mEntryClickHandler); 511 mContactCard.setOnClickListener(mEntryClickHandler); 512 mContactCard.setExpandButtonText( 513 getResources().getString(R.string.expanding_entry_card_view_see_all)); 514 515 mRecentCard.setOnClickListener(mEntryClickHandler); 516 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 517 518 mAboutCard.setOnClickListener(mEntryClickHandler); 519 520 mPhotoView = (ImageView) findViewById(R.id.photo); 521 mTransparentView = findViewById(R.id.transparent_view); 522 if (mScroller != null) { 523 mTransparentView.setOnClickListener(new OnClickListener() { 524 @Override 525 public void onClick(View v) { 526 mScroller.scrollOffBottom(); 527 } 528 }); 529 } 530 531 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 532 setActionBar(toolbar); 533 getActionBar().setTitle(null); 534 // Put a TextView with a known resource id into the ActionBar. This allows us to easily 535 // find the correct TextView location & size later. 536 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null)); 537 538 mHasAlreadyBeenOpened = savedInstanceState != null; 539 540 mWindowScrim = new ColorDrawable(SCRIM_COLOR); 541 getWindow().setBackgroundDrawable(mWindowScrim); 542 if (!mHasAlreadyBeenOpened) { 543 final int duration = getResources().getInteger(android.R.integer.config_shortAnimTime); 544 ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 0xFF).setDuration(duration).start(); 545 } 546 547 mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED); 548 // mScroller needs to perform asynchronous measurements after initalize(), therefore 549 // we can't mark this as GONE. 550 mScroller.setVisibility(View.INVISIBLE); 551 552 setHeaderNameText(R.string.missing_name); 553 554 mSelectAccountFragmentListener= (SelectAccountDialogFragmentListener) getFragmentManager() 555 .findFragmentByTag(FRAGMENT_TAG_SELECT_ACCOUNT); 556 if (mSelectAccountFragmentListener == null) { 557 mSelectAccountFragmentListener = new SelectAccountDialogFragmentListener(); 558 getFragmentManager().beginTransaction().add(0, mSelectAccountFragmentListener, 559 FRAGMENT_TAG_SELECT_ACCOUNT).commit(); 560 mSelectAccountFragmentListener.setRetainInstance(true); 561 } 562 mSelectAccountFragmentListener.setQuickContactActivity(this); 563 564 if (savedInstanceState != null) { 565 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0); 566 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 567 new Runnable() { 568 @Override 569 public void run() { 570 // Need to wait for the pre draw before setting the initial scroll 571 // value. Prior to pre draw all scroll values are invalid. 572 if (mHasAlreadyBeenOpened) { 573 mScroller.setVisibility(View.VISIBLE); 574 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 575 } 576 // Need to wait for pre draw for setting the theme color. Setting the 577 // header tint before the MultiShrinkScroller has been measured will 578 // cause incorrect tinting calculations. 579 if (color != 0) { 580 setThemeColor(MaterialColorMapUtils.calculateSecondaryColor(color)); 581 } 582 } 583 }); 584 } 585 586 Trace.endSection(); 587 } 588 589 @Override 590 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 591 if (requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 592 resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED) { 593 // The contact that we were showing has been deleted. 594 finish(); 595 } 596 } 597 598 @Override 599 protected void onNewIntent(Intent intent) { 600 super.onNewIntent(intent); 601 mHasAlreadyBeenOpened = true; 602 mHasComputedThemeColor = false; 603 processIntent(intent); 604 } 605 606 @Override 607 public void onSaveInstanceState(Bundle savedInstanceState) { 608 super.onSaveInstanceState(savedInstanceState); 609 if (mColorFilter != null) { 610 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilter.getColor()); 611 } 612 } 613 614 private void processIntent(Intent intent) { 615 Uri lookupUri = intent.getData(); 616 617 // Check to see whether it comes from the old version. 618 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 619 final long rawContactId = ContentUris.parseId(lookupUri); 620 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 621 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 622 } 623 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, 624 QuickContact.MODE_LARGE); 625 final Uri oldLookupUri = mLookupUri; 626 627 mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); 628 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 629 if (oldLookupUri == null) { 630 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 631 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 632 } else if (oldLookupUri != mLookupUri) { 633 // After copying a directory contact, the contact URI changes. Therefore, 634 // we need to restart the loader and reload the new contact. 635 for (int interactionLoaderId : mRecentLoaderIds) { 636 getLoaderManager().destroyLoader(interactionLoaderId); 637 } 638 mContactLoader = (ContactLoader) getLoaderManager().restartLoader( 639 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 640 } 641 642 NfcHandler.register(this, mLookupUri); 643 } 644 645 private void runEntranceAnimation() { 646 if (mHasAlreadyBeenOpened) { 647 return; 648 } 649 mHasAlreadyBeenOpened = true; 650 mScroller.scrollUpForEntranceAnimation(mExtraMode != MODE_FULLY_EXPANDED); 651 } 652 653 /** Assign this string to the view if it is not empty. */ 654 private void setHeaderNameText(int resId) { 655 if (mScroller != null) { 656 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString()); 657 } 658 } 659 660 /** Assign this string to the view if it is not empty. */ 661 private void setHeaderNameText(String value) { 662 if (!TextUtils.isEmpty(value)) { 663 if (mScroller != null) { 664 mScroller.setTitle(value); 665 } 666 } 667 } 668 669 /** 670 * Check if the given MIME-type appears in the list of excluded MIME-types 671 * that the most-recent caller requested. 672 */ 673 private boolean isMimeExcluded(String mimeType) { 674 if (mExcludeMimes == null) return false; 675 for (String excludedMime : mExcludeMimes) { 676 if (TextUtils.equals(excludedMime, mimeType)) { 677 return true; 678 } 679 } 680 return false; 681 } 682 683 /** 684 * Handle the result from the ContactLoader 685 */ 686 private void bindContactData(final Contact data) { 687 Trace.beginSection("bindContactData"); 688 mContactData = data; 689 invalidateOptionsMenu(); 690 691 Trace.endSection(); 692 Trace.beginSection("Set display photo & name"); 693 694 mPhotoSetter.setupContactPhoto(data, mPhotoView); 695 extractAndApplyTintFromPhotoViewAsynchronously(); 696 analyzeWhitenessOfPhotoAsynchronously(); 697 setHeaderNameText(data.getDisplayName()); 698 699 Trace.endSection(); 700 701 mEntriesAndActionsTask = new AsyncTask<Void, Void, 702 Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>() { 703 704 @Override 705 protected Pair<List<List<DataItem>>, Map<String, List<DataItem>>> doInBackground( 706 Void... params) { 707 return generateDataModelFromContact(data); 708 } 709 710 @Override 711 protected void onPostExecute(Pair<List<List<DataItem>>, 712 Map<String, List<DataItem>>> dataItemsPair) { 713 super.onPostExecute(dataItemsPair); 714 mDataItemsList = dataItemsPair.first; 715 mDataItemsMap = dataItemsPair.second; 716 // Check that original AsyncTask parameters are still valid and the activity 717 // is still running before binding to UI. A new intent could invalidate 718 // the results, for example. 719 if (data == mContactData && !isCancelled()) { 720 bindDataToCards(); 721 showActivity(); 722 } 723 } 724 }; 725 mEntriesAndActionsTask.execute(); 726 } 727 728 private void bindDataToCards() { 729 startInteractionLoaders(); 730 populateContactAndAboutCard(); 731 } 732 733 private void startInteractionLoaders() { 734 final List<DataItem> phoneDataItems = mDataItemsMap.get(Phone.CONTENT_ITEM_TYPE); 735 String[] phoneNumbers = null; 736 if (phoneDataItems != null) { 737 phoneNumbers = new String[phoneDataItems.size()]; 738 for (int i = 0; i < phoneDataItems.size(); ++i) { 739 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber(); 740 } 741 } 742 final Bundle phonesExtraBundle = new Bundle(); 743 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers); 744 745 Trace.beginSection("start sms loader"); 746 getLoaderManager().initLoader( 747 LOADER_SMS_ID, 748 phonesExtraBundle, 749 mLoaderInteractionsCallbacks); 750 Trace.endSection(); 751 752 Trace.beginSection("start call log loader"); 753 getLoaderManager().initLoader( 754 LOADER_CALL_LOG_ID, 755 phonesExtraBundle, 756 mLoaderInteractionsCallbacks); 757 Trace.endSection(); 758 759 760 Trace.beginSection("start calendar loader"); 761 final List<DataItem> emailDataItems = mDataItemsMap.get(Email.CONTENT_ITEM_TYPE); 762 String[] emailAddresses = null; 763 if (emailDataItems != null) { 764 emailAddresses = new String[emailDataItems.size()]; 765 for (int i = 0; i < emailDataItems.size(); ++i) { 766 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress(); 767 } 768 } 769 final Bundle emailsExtraBundle = new Bundle(); 770 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses); 771 getLoaderManager().initLoader( 772 LOADER_CALENDAR_ID, 773 emailsExtraBundle, 774 mLoaderInteractionsCallbacks); 775 Trace.endSection(); 776 } 777 778 private void showActivity() { 779 if (mScroller != null) { 780 mScroller.setVisibility(View.VISIBLE); 781 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 782 new Runnable() { 783 @Override 784 public void run() { 785 runEntranceAnimation(); 786 } 787 }); 788 } 789 } 790 791 private void populateContactAndAboutCard() { 792 Trace.beginSection("bind contact card"); 793 794 final List<List<Entry>> contactCardEntries = new ArrayList<>(); 795 final List<List<Entry>> aboutCardEntries = new ArrayList<>(); 796 797 for (int i = 0; i < mDataItemsList.size(); ++i) { 798 final List<DataItem> dataItemsByMimeType = mDataItemsList.get(i); 799 final DataItem topDataItem = dataItemsByMimeType.get(0); 800 if (ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) { 801 List<Entry> aboutEntries = dataItemsToEntries(mDataItemsList.get(i)); 802 if (aboutEntries.size() > 0) { 803 aboutCardEntries.add(aboutEntries); 804 } 805 } else { 806 List<Entry> contactEntries = dataItemsToEntries(mDataItemsList.get(i)); 807 if (contactEntries.size() > 0) { 808 contactCardEntries.add(contactEntries); 809 } 810 } 811 } 812 813 if (contactCardEntries.size() > 0) { 814 mContactCard.initialize(contactCardEntries, 815 /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN, 816 /* isExpanded = */ false, 817 mExpandingEntryCardViewListener); 818 mContactCard.setVisibility(View.VISIBLE); 819 } else { 820 mContactCard.setVisibility(View.GONE); 821 } 822 Trace.endSection(); 823 824 Trace.beginSection("bind about card"); 825 mAboutCard.initialize(aboutCardEntries, 826 /* numInitialVisibleEntries = */ 1, 827 /* isExpanded = */ true, 828 mExpandingEntryCardViewListener); 829 830 if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) { 831 initializeNoContactDetailCard(); 832 } else { 833 mNoContactDetailsCard.setVisibility(View.GONE); 834 } 835 836 // If the Recent card is already initialized (all recent data is loaded), show the About 837 // card if it has entries. Otherwise About card visibility will be set in bindRecentData() 838 if (isAllRecentDataLoaded() && aboutCardEntries.size() > 0) { 839 mAboutCard.setVisibility(View.VISIBLE); 840 } 841 Trace.endSection(); 842 } 843 844 /** 845 * Create a card that shows "Add email" and "Add phone number" entries in grey. 846 */ 847 private void initializeNoContactDetailCard() { 848 final Drawable phoneIcon = getResources().getDrawable( 849 R.drawable.ic_phone_24dp).mutate(); 850 final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 851 phoneIcon, getString(R.string.quickcontact_add_phone_number), 852 /* subHeader = */ null, /* text = */ null, getEditContactIntent(), 853 /* alternateIcon = */ null, /* alternateIntent = */ null, 854 /* alternateContentDescription = */ null, /* shouldApplyColor = */ false, 855 /* isEditable = */ false); 856 857 final Drawable emailIcon = getResources().getDrawable( 858 R.drawable.ic_email_24dp).mutate(); 859 final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 860 emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null, 861 /* text = */ null, getEditContactIntent(), /* alternateIcon = */ null, 862 /* alternateIntent = */ null, /* alternateContentDescription = */ null, 863 /* shouldApplyColor = */ false, /* isEditable = */ false); 864 865 final List<List<Entry>> promptEntries = new ArrayList<>(); 866 promptEntries.add(new ArrayList<Entry>(1)); 867 promptEntries.add(new ArrayList<Entry>(1)); 868 promptEntries.get(0).add(phonePromptEntry); 869 promptEntries.get(1).add(emailPromptEntry); 870 871 final int subHeaderTextColor = getResources().getColor( 872 R.color.quickcontact_entry_sub_header_text_color); 873 final PorterDuffColorFilter greyColorFilter = 874 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP); 875 mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ false, 876 mExpandingEntryCardViewListener); 877 mNoContactDetailsCard.setVisibility(View.VISIBLE); 878 mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor); 879 mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter); 880 } 881 882 /** 883 * Builds the {@link DataItem}s Map out of the Contact. 884 * @param data The contact to build the data from. 885 * @return A pair containing a list of data items sorted within mimetype and sorted 886 * amongst mimetype. The map goes from mimetype string to the sorted list of data items within 887 * mimetype 888 */ 889 private Pair<List<List<DataItem>>, Map<String, List<DataItem>>> generateDataModelFromContact( 890 Contact data) { 891 Trace.beginSection("Build data items map"); 892 893 final Map<String, List<DataItem>> dataItemsMap = new HashMap<>(); 894 895 final ResolveCache cache = ResolveCache.getInstance(this); 896 for (RawContact rawContact : data.getRawContacts()) { 897 for (DataItem dataItem : rawContact.getDataItems()) { 898 dataItem.setRawContactId(rawContact.getId()); 899 900 final String mimeType = dataItem.getMimeType(); 901 if (mimeType == null) continue; 902 903 final AccountType accountType = rawContact.getAccountType(this); 904 final DataKind dataKind = AccountTypeManager.getInstance(this) 905 .getKindOrFallback(accountType, mimeType); 906 if (dataKind == null) continue; 907 908 dataItem.setDataKind(dataKind); 909 910 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this, 911 dataKind)); 912 913 if (isMimeExcluded(mimeType) || !hasData) continue; 914 915 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType); 916 if (dataItemListByType == null) { 917 dataItemListByType = new ArrayList<>(); 918 dataItemsMap.put(mimeType, dataItemListByType); 919 } 920 dataItemListByType.add(dataItem); 921 } 922 } 923 Trace.endSection(); 924 925 Trace.beginSection("sort within mimetypes"); 926 /* 927 * Sorting is a multi part step. The end result is to a have a sorted list of the most 928 * used data items, one per mimetype. Then, within each mimetype, the list of data items 929 * for that type is also sorted, based off of {super primary, primary, times used} in that 930 * order. 931 */ 932 final List<List<DataItem>> dataItemsList = new ArrayList<>(); 933 for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) { 934 // Remove duplicate data items 935 Collapser.collapseList(mimeTypeDataItems, this); 936 // Sort within mimetype 937 Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator); 938 // Add to the list of data item lists 939 dataItemsList.add(mimeTypeDataItems); 940 } 941 Trace.endSection(); 942 943 Trace.beginSection("sort amongst mimetypes"); 944 // Sort amongst mimetypes to bubble up the top data items for the contact card 945 Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator); 946 Trace.endSection(); 947 948 return new Pair<>(dataItemsList, dataItemsMap); 949 } 950 951 /** 952 * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display. 953 * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned. 954 * @param dataItem The {@link DataItem} to convert. 955 * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present. 956 */ 957 private Entry dataItemToEntry(DataItem dataItem) { 958 Drawable icon = null; 959 String header = null; 960 String subHeader = null; 961 Drawable subHeaderIcon = null; 962 String text = null; 963 Drawable textIcon = null; 964 Intent intent = null; 965 boolean shouldApplyColor = true; 966 Drawable alternateIcon = null; 967 Intent alternateIntent = null; 968 String alternateContentDescription = null; 969 final boolean isEditable = false; 970 971 DataKind kind = dataItem.getDataKind(); 972 973 if (dataItem instanceof ImDataItem) { 974 final ImDataItem im = (ImDataItem) dataItem; 975 intent = ContactsUtils.buildImIntent(this, im).first; 976 header = getResources().getString(R.string.header_im_entry); 977 final boolean isEmail = im.isCreatedFromEmail(); 978 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); 979 subHeader = Im.getProtocolLabel(getResources(), protocol, 980 im.getCustomProtocol()).toString(); 981 } else if (dataItem instanceof OrganizationDataItem) { 982 final OrganizationDataItem organization = (OrganizationDataItem) dataItem; 983 header = getResources().getString(R.string.header_organization_entry); 984 subHeader = organization.getCompany(); 985 text = organization.getTitle(); 986 } else if (dataItem instanceof NicknameDataItem) { 987 final NicknameDataItem nickname = (NicknameDataItem) dataItem; 988 // Build nickname entries 989 final boolean isNameRawContact = 990 (mContactData.getNameRawContactId() == dataItem.getRawContactId()); 991 992 final boolean duplicatesTitle = 993 isNameRawContact 994 && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 995 996 if (!duplicatesTitle) { 997 header = getResources().getString(R.string.header_nickname_entry); 998 subHeader = nickname.getName(); 999 } 1000 } else if (dataItem instanceof NoteDataItem) { 1001 final NoteDataItem note = (NoteDataItem) dataItem; 1002 header = getResources().getString(R.string.header_note_entry); 1003 subHeader = note.getNote(); 1004 } else if (dataItem instanceof WebsiteDataItem) { 1005 final WebsiteDataItem website = (WebsiteDataItem) dataItem; 1006 header = getResources().getString(R.string.header_website_entry); 1007 subHeader = website.getUrl(); 1008 try { 1009 final WebAddress webAddress = new WebAddress(website.buildDataString(this, kind)); 1010 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString())); 1011 } catch (final ParseException e) { 1012 Log.e(TAG, "Couldn't parse website: " + website.buildDataString(this, kind)); 1013 } 1014 } else if (dataItem instanceof EventDataItem) { 1015 final EventDataItem event = (EventDataItem) dataItem; 1016 final String dataString = event.buildDataString(this, kind); 1017 final Calendar cal = DateUtils.parseDate(dataString, false); 1018 if (cal != null) { 1019 final Date nextAnniversary = 1020 DateUtils.getNextAnnualDate(cal); 1021 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 1022 builder.appendPath("time"); 1023 ContentUris.appendId(builder, nextAnniversary.getTime()); 1024 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); 1025 } 1026 header = getResources().getString(R.string.header_event_entry); 1027 if (event.hasKindTypeColumn(kind)) { 1028 subHeader = getResources().getString(Event.getTypeResource( 1029 event.getKindTypeColumn(kind))); 1030 } 1031 text = DateUtils.formatDate(this, dataString); 1032 } else if (dataItem instanceof RelationDataItem) { 1033 final RelationDataItem relation = (RelationDataItem) dataItem; 1034 final String dataString = relation.buildDataString(this, kind); 1035 if (!TextUtils.isEmpty(dataString)) { 1036 intent = new Intent(Intent.ACTION_SEARCH); 1037 intent.putExtra(SearchManager.QUERY, dataString); 1038 intent.setType(Contacts.CONTENT_TYPE); 1039 } 1040 header = getResources().getString(R.string.header_relation_entry); 1041 subHeader = relation.getName(); 1042 if (relation.hasKindTypeColumn(kind)) { 1043 text = Relation.getTypeLabel(getResources(), relation.getKindTypeColumn(kind), 1044 relation.getLabel()).toString(); 1045 } 1046 } else if (dataItem instanceof PhoneDataItem) { 1047 final PhoneDataItem phone = (PhoneDataItem) dataItem; 1048 if (!TextUtils.isEmpty(phone.getNumber())) { 1049 header = phone.buildDataString(this, kind); 1050 if (phone.hasKindTypeColumn(kind)) { 1051 text = Phone.getTypeLabel(getResources(), phone.getKindTypeColumn(kind), 1052 phone.getLabel()).toString(); 1053 } 1054 icon = getResources().getDrawable(R.drawable.ic_phone_24dp); 1055 if (PhoneCapabilityTester.isPhone(this)) { 1056 intent = CallUtil.getCallIntent(phone.getNumber()); 1057 } 1058 alternateIntent = new Intent(Intent.ACTION_SENDTO, 1059 Uri.fromParts(CallUtil.SCHEME_SMSTO, phone.getNumber(), null)); 1060 alternateIcon = getResources().getDrawable(R.drawable.ic_message_24dp); 1061 alternateContentDescription = getResources().getString(R.string.sms_other); 1062 } 1063 } else if (dataItem instanceof EmailDataItem) { 1064 final EmailDataItem email = (EmailDataItem) dataItem; 1065 final String address = email.getData(); 1066 if (!TextUtils.isEmpty(address)) { 1067 final Uri mailUri = Uri.fromParts(CallUtil.SCHEME_MAILTO, address, null); 1068 intent = new Intent(Intent.ACTION_SENDTO, mailUri); 1069 header = email.getAddress(); 1070 if (email.hasKindTypeColumn(kind)) { 1071 text = Email.getTypeLabel(getResources(), email.getKindTypeColumn(kind), 1072 email.getLabel()).toString(); 1073 } 1074 icon = getResources().getDrawable(R.drawable.ic_email_24dp); 1075 } 1076 } else if (dataItem instanceof StructuredPostalDataItem) { 1077 StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem; 1078 final String postalAddress = postal.getFormattedAddress(); 1079 if (!TextUtils.isEmpty(postalAddress)) { 1080 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress); 1081 header = postal.getFormattedAddress(); 1082 if (postal.hasKindTypeColumn(kind)) { 1083 text = StructuredPostal.getTypeLabel(getResources(), 1084 postal.getKindTypeColumn(kind), postal.getLabel()).toString(); 1085 } 1086 icon = getResources().getDrawable(R.drawable.ic_place_24dp); 1087 } 1088 } else if (dataItem instanceof SipAddressDataItem) { 1089 if (PhoneCapabilityTester.isSipPhone(this)) { 1090 final SipAddressDataItem sip = (SipAddressDataItem) dataItem; 1091 final String address = sip.getSipAddress(); 1092 if (!TextUtils.isEmpty(address)) { 1093 final Uri callUri = Uri.fromParts(CallUtil.SCHEME_SIP, address, null); 1094 intent = CallUtil.getCallIntent(callUri); 1095 // Note that this item will get a SIP-specific variant 1096 // of the "call phone" icon, rather than the standard 1097 // app icon for the Phone app (which we show for 1098 // regular phone numbers.) That's because the phone 1099 // app explicitly specifies an android:icon attribute 1100 // for the SIP-related intent-filters in its manifest. 1101 } 1102 icon = ResolveCache.getInstance(this).getIcon(sip.getMimeType(), intent); 1103 // Call mutate to create a new Drawable.ConstantState for color filtering 1104 if (icon != null) { 1105 icon.mutate(); 1106 } 1107 } 1108 } else if (dataItem instanceof StructuredNameDataItem) { 1109 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName(); 1110 if (!TextUtils.isEmpty(givenName)) { 1111 mAboutCard.setTitle(getResources().getString(R.string.about_card_title) + 1112 " " + givenName); 1113 } else { 1114 mAboutCard.setTitle(getResources().getString(R.string.about_card_title)); 1115 } 1116 } else { 1117 // Custom DataItem 1118 header = dataItem.buildDataStringForDisplay(this, kind); 1119 text = kind.typeColumn; 1120 intent = new Intent(Intent.ACTION_VIEW); 1121 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId()); 1122 intent.setDataAndType(uri, dataItem.getMimeType()); 1123 1124 if (intent != null) { 1125 final String mimetype = intent.getType(); 1126 1127 // Attempt to use known icons for known 3p types. Otherwise default to ResolveCache 1128 switch (mimetype) { 1129 case MIMETYPE_GPLUS_PROFILE: 1130 if (INTENT_DATA_GPLUS_PROFILE_ADD_TO_CIRCLE.equals( 1131 intent.getDataString())) { 1132 icon = getResources().getDrawable( 1133 R.drawable.ic_add_to_circles_black_24); 1134 } else { 1135 icon = getResources().getDrawable(R.drawable.ic_google_plus_24dp); 1136 } 1137 break; 1138 case MIMETYPE_HANGOUTS: 1139 if (INTENT_DATA_HANGOUTS_VIDEO.equals(intent.getDataString())) { 1140 icon = getResources().getDrawable(R.drawable.ic_hangout_video_24dp); 1141 } else { 1142 icon = getResources().getDrawable(R.drawable.ic_hangout_24dp); 1143 } 1144 break; 1145 default: 1146 icon = ResolveCache.getInstance(this).getIcon( 1147 dataItem.getMimeType(), intent); 1148 // Call mutate to create a new Drawable.ConstantState for color filtering 1149 if (icon != null) { 1150 icon.mutate(); 1151 } 1152 shouldApplyColor = false; 1153 } 1154 } 1155 } 1156 1157 if (intent != null) { 1158 // Do not set the intent is there are no resolves 1159 if (!PhoneCapabilityTester.isIntentRegistered(this, intent)) { 1160 intent = null; 1161 } 1162 } 1163 1164 if (alternateIntent != null) { 1165 // Do not set the alternate intent is there are no resolves 1166 if (!PhoneCapabilityTester.isIntentRegistered(this, alternateIntent)) { 1167 alternateIntent = null; 1168 } 1169 1170 // Attempt to use package manager to find a suitable content description if needed 1171 if (TextUtils.isEmpty(alternateContentDescription)) { 1172 alternateContentDescription = getIntentResolveLabel(alternateIntent); 1173 } 1174 } 1175 1176 // If the Entry has no visual elements, return null 1177 if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) && 1178 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) { 1179 return null; 1180 } 1181 1182 final int dataId = dataItem.getId() > Integer.MAX_VALUE ? 1183 -1 : (int) dataItem.getId(); 1184 1185 return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon, intent, 1186 alternateIcon, alternateIntent, alternateContentDescription, shouldApplyColor, 1187 isEditable); 1188 } 1189 1190 private List<Entry> dataItemsToEntries(List<DataItem> dataItems) { 1191 final List<Entry> entries = new ArrayList<>(); 1192 for (DataItem dataItem : dataItems) { 1193 final Entry entry = dataItemToEntry(dataItem); 1194 if (entry != null) { 1195 entries.add(entry); 1196 } 1197 } 1198 return entries; 1199 } 1200 1201 private String getIntentResolveLabel(Intent intent) { 1202 final List<ResolveInfo> matches = getPackageManager().queryIntentActivities(intent, 1203 PackageManager.MATCH_DEFAULT_ONLY); 1204 1205 // Pick first match, otherwise best found 1206 ResolveInfo bestResolve = null; 1207 final int size = matches.size(); 1208 if (size == 1) { 1209 bestResolve = matches.get(0); 1210 } else if (size > 1) { 1211 bestResolve = ResolveCache.getInstance(this).getBestResolve(intent, matches); 1212 } 1213 1214 if (bestResolve == null) { 1215 return null; 1216 } 1217 1218 return String.valueOf(bestResolve.loadLabel(getPackageManager())); 1219 } 1220 1221 /** 1222 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 1223 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 1224 * on a Nexus 5. 1225 */ 1226 private void extractAndApplyTintFromPhotoViewAsynchronously() { 1227 if (mScroller == null) { 1228 return; 1229 } 1230 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1231 new AsyncTask<Void, Void, MaterialPalette>() { 1232 @Override 1233 protected MaterialPalette doInBackground(Void... params) { 1234 1235 if (imageViewDrawable instanceof BitmapDrawable) { 1236 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 1237 final int primaryColor = colorFromBitmap(bitmap); 1238 if (primaryColor != 0) { 1239 return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 1240 primaryColor); 1241 } 1242 } 1243 if (imageViewDrawable instanceof LetterTileDrawable) { 1244 final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor(); 1245 return MaterialColorMapUtils.calculateSecondaryColor(primaryColor); 1246 } 1247 return MaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 1248 getResources().getColor(R.color.quickcontact_default_photo_tint_color)); 1249 } 1250 1251 @Override 1252 protected void onPostExecute(MaterialPalette palette) { 1253 super.onPostExecute(palette); 1254 if (mHasComputedThemeColor) { 1255 // If we had previously computed a theme color from the contact photo, 1256 // then do not update the theme color. Changing the theme color several 1257 // seconds after QC has started, as a result of an updated/upgraded photo, 1258 // is a jarring experience. On the other hand, changing the theme color after 1259 // a rotation or onNewIntent() is perfectly fine. 1260 return; 1261 } 1262 // Check that the Photo has not changed. If it has changed, the new tint 1263 // color needs to be extracted 1264 if (imageViewDrawable == mPhotoView.getDrawable()) { 1265 mHasComputedThemeColor = true; 1266 setThemeColor(palette); 1267 } 1268 } 1269 }.execute(); 1270 } 1271 1272 /** 1273 * Examine how many white pixels are in the bitmap in order to determine whether or not 1274 * we need gradient overlays on top of the image. 1275 */ 1276 private void analyzeWhitenessOfPhotoAsynchronously() { 1277 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 1278 new AsyncTask<Void, Void, Boolean>() { 1279 @Override 1280 protected Boolean doInBackground(Void... params) { 1281 if (imageViewDrawable instanceof BitmapDrawable) { 1282 final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap(); 1283 return WhitenessUtils.isBitmapWhiteAtTopOrBottom(bitmap); 1284 } 1285 return !(imageViewDrawable instanceof LetterTileDrawable); 1286 } 1287 1288 @Override 1289 protected void onPostExecute(Boolean isWhite) { 1290 super.onPostExecute(isWhite); 1291 mScroller.setUseGradient(isWhite); 1292 } 1293 }.execute(); 1294 } 1295 1296 private void setThemeColor(MaterialPalette palette) { 1297 // If the color is invalid, use the predefined default 1298 final int primaryColor = palette.mPrimaryColor; 1299 mScroller.setHeaderTintColor(primaryColor); 1300 mStatusBarColor = palette.mSecondaryColor; 1301 updateStatusBarColor(); 1302 1303 mColorFilter = 1304 new PorterDuffColorFilter(primaryColor, PorterDuff.Mode.SRC_ATOP); 1305 mContactCard.setColorAndFilter(primaryColor, mColorFilter); 1306 mRecentCard.setColorAndFilter(primaryColor, mColorFilter); 1307 mAboutCard.setColorAndFilter(primaryColor, mColorFilter); 1308 } 1309 1310 private void updateStatusBarColor() { 1311 if (mScroller == null) { 1312 return; 1313 } 1314 final int desiredStatusBarColor; 1315 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 1316 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 1317 desiredStatusBarColor = mStatusBarColor; 1318 } else { 1319 desiredStatusBarColor = Color.TRANSPARENT; 1320 } 1321 // Animate to the new color. 1322 if (desiredStatusBarColor != getWindow().getStatusBarColor()) { 1323 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 1324 getWindow().getStatusBarColor(), desiredStatusBarColor); 1325 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 1326 animation.setEvaluator(new ArgbEvaluator()); 1327 animation.start(); 1328 } 1329 } 1330 1331 private int colorFromBitmap(Bitmap bitmap) { 1332 // Author of Palette recommends using 24 colors when analyzing profile photos. 1333 final int NUMBER_OF_PALETTE_COLORS = 24; 1334 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 1335 if (palette != null && palette.getVibrantSwatch() != null) { 1336 return palette.getVibrantSwatch().getRgb(); 1337 } 1338 return 0; 1339 } 1340 1341 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 1342 final List<Entry> entries = new ArrayList<>(); 1343 for (ContactInteraction interaction : interactions) { 1344 entries.add(new Entry(/* id = */ -1, 1345 interaction.getIcon(this), 1346 interaction.getViewHeader(this), 1347 interaction.getViewBody(this), 1348 interaction.getBodyIcon(this), 1349 interaction.getViewFooter(this), 1350 interaction.getFooterIcon(this), 1351 interaction.getIntent(), 1352 /* alternateIcon = */ null, 1353 /* alternateIntent = */ null, 1354 /* alternateContentDescription = */ null, 1355 /* shouldApplyColor = */ true, 1356 /* isEditable = */ false)); 1357 } 1358 return entries; 1359 } 1360 1361 private final LoaderCallbacks<Contact> mLoaderContactCallbacks = 1362 new LoaderCallbacks<Contact>() { 1363 @Override 1364 public void onLoaderReset(Loader<Contact> loader) { 1365 mContactData = null; 1366 } 1367 1368 @Override 1369 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1370 Trace.beginSection("onLoadFinished()"); 1371 1372 if (isFinishing()) { 1373 return; 1374 } 1375 if (data.isError()) { 1376 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} 1377 // should log the actual exception. 1378 throw new IllegalStateException("Failed to load contact", data.getException()); 1379 } 1380 if (data.isNotFound()) { 1381 if (mHasAlreadyBeenOpened) { 1382 finish(); 1383 } else { 1384 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 1385 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 1386 Toast.LENGTH_LONG).show(); 1387 } 1388 return; 1389 } 1390 1391 bindContactData(data); 1392 1393 Trace.endSection(); 1394 } 1395 1396 @Override 1397 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1398 if (mLookupUri == null) { 1399 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 1400 } 1401 // Load all contact data. We need loadGroupMetaData=true to determine whether the 1402 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 1403 return new ContactLoader(getApplicationContext(), mLookupUri, 1404 true /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, 1405 true /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); 1406 } 1407 }; 1408 1409 @Override 1410 public void onBackPressed() { 1411 if (mScroller != null) { 1412 if (!mIsExitAnimationInProgress) { 1413 mScroller.scrollOffBottom(); 1414 } 1415 } else { 1416 super.onBackPressed(); 1417 } 1418 } 1419 1420 @Override 1421 public void finish() { 1422 super.finish(); 1423 1424 // override transitions to skip the standard window animations 1425 overridePendingTransition(0, 0); 1426 } 1427 1428 private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 1429 new LoaderCallbacks<List<ContactInteraction>>() { 1430 1431 @Override 1432 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 1433 Log.v(TAG, "onCreateLoader"); 1434 Loader<List<ContactInteraction>> loader = null; 1435 switch (id) { 1436 case LOADER_SMS_ID: 1437 Log.v(TAG, "LOADER_SMS_ID"); 1438 loader = new SmsInteractionsLoader( 1439 QuickContactActivity.this, 1440 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1441 MAX_SMS_RETRIEVE); 1442 break; 1443 case LOADER_CALENDAR_ID: 1444 Log.v(TAG, "LOADER_CALENDAR_ID"); 1445 final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS); 1446 List<String> emailsList = null; 1447 if (emailsArray != null) { 1448 emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)); 1449 } 1450 loader = new CalendarInteractionsLoader( 1451 QuickContactActivity.this, 1452 emailsList, 1453 MAX_FUTURE_CALENDAR_RETRIEVE, 1454 MAX_PAST_CALENDAR_RETRIEVE, 1455 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 1456 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 1457 break; 1458 case LOADER_CALL_LOG_ID: 1459 Log.v(TAG, "LOADER_CALL_LOG_ID"); 1460 loader = new CallLogInteractionsLoader( 1461 QuickContactActivity.this, 1462 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 1463 MAX_CALL_LOG_RETRIEVE); 1464 } 1465 return loader; 1466 } 1467 1468 @Override 1469 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 1470 List<ContactInteraction> data) { 1471 mRecentLoaderResults.put(loader.getId(), data); 1472 1473 if (isAllRecentDataLoaded()) { 1474 bindRecentData(); 1475 } 1476 } 1477 1478 @Override 1479 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 1480 mRecentLoaderResults.remove(loader.getId()); 1481 } 1482 }; 1483 1484 private boolean isAllRecentDataLoaded() { 1485 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 1486 } 1487 1488 private void bindRecentData() { 1489 final List<ContactInteraction> allInteractions = new ArrayList<>(); 1490 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 1491 allInteractions.addAll(loaderInteractions); 1492 } 1493 1494 // Sort the interactions by most recent 1495 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 1496 @Override 1497 public int compare(ContactInteraction a, ContactInteraction b) { 1498 return a.getInteractionDate() >= b.getInteractionDate() ? -1 : 1; 1499 } 1500 }); 1501 1502 1503 List<List<Entry>> interactionsWrapper = new ArrayList<>(); 1504 interactionsWrapper.add(contactInteractionsToEntries(allInteractions)); 1505 if (allInteractions.size() > 0) { 1506 mRecentCard.initialize(interactionsWrapper, 1507 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 1508 /* isExpanded = */ false, mExpandingEntryCardViewListener); 1509 mRecentCard.setVisibility(View.VISIBLE); 1510 } 1511 1512 // About card is initialized along with the contact card, but since it appears after 1513 // the recent card in the UI, we hold off until making it visible until the recent card 1514 // is also ready to avoid stuttering. 1515 if (mAboutCard.shouldShow()) { 1516 mAboutCard.setVisibility(View.VISIBLE); 1517 } else { 1518 mAboutCard.setVisibility(View.GONE); 1519 } 1520 } 1521 1522 @Override 1523 protected void onStop() { 1524 super.onStop(); 1525 1526 if (mEntriesAndActionsTask != null) { 1527 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 1528 // results on the UI thread. In some circumstances Activities are killed without 1529 // onStop() being called. This is not a problem, because in these circumstances 1530 // the entire process will be killed. 1531 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 1532 } 1533 } 1534 1535 /** 1536 * Returns true if it is possible to edit the current contact. 1537 */ 1538 private boolean isContactEditable() { 1539 return mContactData != null && !mContactData.isDirectoryEntry(); 1540 } 1541 1542 private Intent getEditContactIntent() { 1543 final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); 1544 mContactLoader.cacheResult(); 1545 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 1546 return intent; 1547 } 1548 1549 private void editContact() { 1550 startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 1551 } 1552 1553 private void toggleStar(MenuItem starredMenuItem) { 1554 // Make sure there is a contact 1555 if (mLookupUri != null) { 1556 // Read the current starred value from the UI instead of using the last 1557 // loaded state. This allows rapid tapping without writing the same 1558 // value several times 1559 final boolean isStarred = starredMenuItem.isChecked(); 1560 1561 // To improve responsiveness, swap out the picture (and tag) in the UI already 1562 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1563 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1564 !isStarred); 1565 1566 // Now perform the real save 1567 final Intent intent = ContactSaveService.createSetStarredIntent( 1568 QuickContactActivity.this, mLookupUri, !isStarred); 1569 startService(intent); 1570 } 1571 } 1572 1573 /** 1574 * Calls into the contacts provider to get a pre-authorized version of the given URI. 1575 */ 1576 private Uri getPreAuthorizedUri(Uri uri) { 1577 final Bundle uriBundle = new Bundle(); 1578 uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri); 1579 final Bundle authResponse = getContentResolver().call( 1580 ContactsContract.AUTHORITY_URI, 1581 ContactsContract.Authorization.AUTHORIZATION_METHOD, 1582 null, 1583 uriBundle); 1584 if (authResponse != null) { 1585 return (Uri) authResponse.getParcelable( 1586 ContactsContract.Authorization.KEY_AUTHORIZED_URI); 1587 } else { 1588 return uri; 1589 } 1590 } 1591 1592 private void shareContact() { 1593 final String lookupKey = mContactData.getLookupKey(); 1594 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 1595 if (mContactData.isUserProfile()) { 1596 // User is sharing the profile. We don't want to force the receiver to have 1597 // the highly-privileged READ_PROFILE permission, so we need to request a 1598 // pre-authorized URI from the provider. 1599 shareUri = getPreAuthorizedUri(shareUri); 1600 } 1601 1602 final Intent intent = new Intent(Intent.ACTION_SEND); 1603 intent.setType(Contacts.CONTENT_VCARD_TYPE); 1604 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 1605 1606 // Launch chooser to share contact via 1607 final CharSequence chooseTitle = getText(R.string.share_via); 1608 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 1609 1610 try { 1611 this.startActivity(chooseIntent); 1612 } catch (final ActivityNotFoundException ex) { 1613 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1614 } 1615 } 1616 1617 /** 1618 * Creates a launcher shortcut with the current contact. 1619 */ 1620 private void createLauncherShortcutWithContact() { 1621 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 1622 new OnShortcutIntentCreatedListener() { 1623 1624 @Override 1625 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 1626 // Broadcast the shortcutIntent to the launcher to create a 1627 // shortcut to this contact 1628 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 1629 QuickContactActivity.this.sendBroadcast(shortcutIntent); 1630 1631 // Send a toast to give feedback to the user that a shortcut to this 1632 // contact was added to the launcher. 1633 Toast.makeText(QuickContactActivity.this, 1634 R.string.createContactShortcutSuccessful, 1635 Toast.LENGTH_SHORT).show(); 1636 } 1637 1638 }); 1639 builder.createContactShortcutIntent(mLookupUri); 1640 } 1641 1642 @Override 1643 public boolean onCreateOptionsMenu(Menu menu) { 1644 final MenuInflater inflater = getMenuInflater(); 1645 inflater.inflate(R.menu.quickcontact, menu); 1646 return true; 1647 } 1648 1649 @Override 1650 public boolean onPrepareOptionsMenu(Menu menu) { 1651 if (mContactData != null) { 1652 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 1653 ContactDetailDisplayUtils.configureStarredMenuItem(starredMenuItem, 1654 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1655 mContactData.getStarred()); 1656 // Configure edit MenuItem 1657 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 1658 editMenuItem.setVisible(true); 1659 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 1660 .isInvisibleAndAddable(mContactData, this)) { 1661 editMenuItem.setIcon(R.drawable.ic_person_add_tinted_24dp); 1662 editMenuItem.setTitle(R.string.menu_add_contact); 1663 } else if (isContactEditable()) { 1664 editMenuItem.setIcon(R.drawable.ic_create_24dp); 1665 editMenuItem.setTitle(R.string.menu_editContact); 1666 } else { 1667 editMenuItem.setVisible(false); 1668 } 1669 return true; 1670 } 1671 return false; 1672 } 1673 1674 @Override 1675 public boolean onOptionsItemSelected(MenuItem item) { 1676 switch (item.getItemId()) { 1677 case R.id.menu_star: 1678 toggleStar(item); 1679 return true; 1680 case R.id.menu_edit: 1681 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 1682 DirectoryContactUtil.addToMyContacts(mContactData, this, getFragmentManager(), 1683 mSelectAccountFragmentListener); 1684 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1685 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 1686 } else if (isContactEditable()) { 1687 editContact(); 1688 } 1689 return true; 1690 case R.id.menu_share: 1691 shareContact(); 1692 return true; 1693 case R.id.menu_create_contact_shortcut: 1694 createLauncherShortcutWithContact(); 1695 return true; 1696 default: 1697 return super.onOptionsItemSelected(item); 1698 } 1699 } 1700} 1701