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