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