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