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