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