1/* 2 3 * Copyright (C) 2009 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.contacts.quickcontact; 19 20import android.Manifest; 21import android.accounts.Account; 22import android.animation.ArgbEvaluator; 23import android.animation.ObjectAnimator; 24import android.app.Activity; 25import android.app.LoaderManager.LoaderCallbacks; 26import android.app.ProgressDialog; 27import android.app.SearchManager; 28import android.content.ActivityNotFoundException; 29import android.content.BroadcastReceiver; 30import android.content.ContentUris; 31import android.content.ContentValues; 32import android.content.Context; 33import android.content.Intent; 34import android.content.IntentFilter; 35import android.content.Loader; 36import android.content.pm.PackageManager; 37import android.content.pm.ResolveInfo; 38import android.content.pm.ShortcutInfo; 39import android.content.pm.ShortcutManager; 40import android.content.res.Resources; 41import android.graphics.Bitmap; 42import android.graphics.BitmapFactory; 43import android.graphics.Color; 44import android.graphics.PorterDuff; 45import android.graphics.PorterDuffColorFilter; 46import android.graphics.drawable.BitmapDrawable; 47import android.graphics.drawable.ColorDrawable; 48import android.graphics.drawable.Drawable; 49import android.media.RingtoneManager; 50import android.net.Uri; 51import android.os.AsyncTask; 52import android.os.Bundle; 53import android.os.Trace; 54import android.provider.CalendarContract; 55import android.provider.ContactsContract.CommonDataKinds.Email; 56import android.provider.ContactsContract.CommonDataKinds.Event; 57import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 58import android.provider.ContactsContract.CommonDataKinds.Identity; 59import android.provider.ContactsContract.CommonDataKinds.Im; 60import android.provider.ContactsContract.CommonDataKinds.Nickname; 61import android.provider.ContactsContract.CommonDataKinds.Note; 62import android.provider.ContactsContract.CommonDataKinds.Organization; 63import android.provider.ContactsContract.CommonDataKinds.Phone; 64import android.provider.ContactsContract.CommonDataKinds.Relation; 65import android.provider.ContactsContract.CommonDataKinds.SipAddress; 66import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 67import android.provider.ContactsContract.CommonDataKinds.Website; 68import android.provider.ContactsContract.Contacts; 69import android.provider.ContactsContract.Data; 70import android.provider.ContactsContract.DataUsageFeedback; 71import android.provider.ContactsContract.Directory; 72import android.provider.ContactsContract.DisplayNameSources; 73import android.provider.ContactsContract.Intents; 74import android.provider.ContactsContract.QuickContact; 75import android.provider.ContactsContract.RawContacts; 76import android.support.v4.app.ActivityCompat; 77import android.support.v4.content.LocalBroadcastManager; 78import android.support.v4.content.res.ResourcesCompat; 79import android.support.v4.os.BuildCompat; 80import android.support.v7.graphics.Palette; 81import android.telecom.PhoneAccount; 82import android.telecom.TelecomManager; 83import android.text.BidiFormatter; 84import android.text.Spannable; 85import android.text.SpannableString; 86import android.text.TextDirectionHeuristics; 87import android.text.TextUtils; 88import android.util.Log; 89import android.view.ContextMenu; 90import android.view.ContextMenu.ContextMenuInfo; 91import android.view.Menu; 92import android.view.MenuInflater; 93import android.view.MenuItem; 94import android.view.MotionEvent; 95import android.view.View; 96import android.view.View.OnClickListener; 97import android.view.View.OnCreateContextMenuListener; 98import android.view.WindowManager; 99import android.widget.Toast; 100import android.widget.Toolbar; 101 102import com.android.contacts.CallUtil; 103import com.android.contacts.ClipboardUtils; 104import com.android.contacts.Collapser; 105import com.android.contacts.ContactSaveService; 106import com.android.contacts.ContactsActivity; 107import com.android.contacts.ContactsUtils; 108import com.android.contacts.DynamicShortcuts; 109import com.android.contacts.NfcHandler; 110import com.android.contacts.R; 111import com.android.contacts.ShortcutIntentBuilder; 112import com.android.contacts.ShortcutIntentBuilder.OnShortcutIntentCreatedListener; 113import com.android.contacts.activities.ContactEditorActivity; 114import com.android.contacts.activities.ContactSelectionActivity; 115import com.android.contacts.activities.RequestDesiredPermissionsActivity; 116import com.android.contacts.activities.RequestPermissionsActivity; 117import com.android.contacts.compat.CompatUtils; 118import com.android.contacts.compat.EventCompat; 119import com.android.contacts.compat.MultiWindowCompat; 120import com.android.contacts.detail.ContactDisplayUtils; 121import com.android.contacts.dialog.CallSubjectDialog; 122import com.android.contacts.editor.ContactEditorFragment; 123import com.android.contacts.editor.EditorIntents; 124import com.android.contacts.editor.EditorUiUtils; 125import com.android.contacts.interactions.CalendarInteractionsLoader; 126import com.android.contacts.interactions.CallLogInteractionsLoader; 127import com.android.contacts.interactions.ContactDeletionInteraction; 128import com.android.contacts.interactions.ContactInteraction; 129import com.android.contacts.interactions.SmsInteractionsLoader; 130import com.android.contacts.interactions.TouchPointManager; 131import com.android.contacts.lettertiles.LetterTileDrawable; 132import com.android.contacts.list.UiIntentActions; 133import com.android.contacts.logging.Logger; 134import com.android.contacts.logging.QuickContactEvent.ActionType; 135import com.android.contacts.logging.QuickContactEvent.CardType; 136import com.android.contacts.logging.QuickContactEvent.ContactType; 137import com.android.contacts.logging.ScreenEvent.ScreenType; 138import com.android.contacts.model.AccountTypeManager; 139import com.android.contacts.model.Contact; 140import com.android.contacts.model.ContactLoader; 141import com.android.contacts.model.RawContact; 142import com.android.contacts.model.account.AccountType; 143import com.android.contacts.model.dataitem.CustomDataItem; 144import com.android.contacts.model.dataitem.DataItem; 145import com.android.contacts.model.dataitem.DataKind; 146import com.android.contacts.model.dataitem.EmailDataItem; 147import com.android.contacts.model.dataitem.EventDataItem; 148import com.android.contacts.model.dataitem.ImDataItem; 149import com.android.contacts.model.dataitem.NicknameDataItem; 150import com.android.contacts.model.dataitem.NoteDataItem; 151import com.android.contacts.model.dataitem.OrganizationDataItem; 152import com.android.contacts.model.dataitem.PhoneDataItem; 153import com.android.contacts.model.dataitem.RelationDataItem; 154import com.android.contacts.model.dataitem.SipAddressDataItem; 155import com.android.contacts.model.dataitem.StructuredNameDataItem; 156import com.android.contacts.model.dataitem.StructuredPostalDataItem; 157import com.android.contacts.model.dataitem.WebsiteDataItem; 158import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; 159import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryContextMenuInfo; 160import com.android.contacts.quickcontact.ExpandingEntryCardView.EntryTag; 161import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener; 162import com.android.contacts.quickcontact.WebAddress.ParseException; 163import com.android.contacts.util.DateUtils; 164import com.android.contacts.util.ImageViewDrawableSetter; 165import com.android.contacts.util.ImplicitIntentsUtil; 166import com.android.contacts.util.MaterialColorMapUtils; 167import com.android.contacts.util.MaterialColorMapUtils.MaterialPalette; 168import com.android.contacts.util.PermissionsUtil; 169import com.android.contacts.util.PhoneCapabilityTester; 170import com.android.contacts.util.SchedulingUtils; 171import com.android.contacts.util.SharedPreferenceUtil; 172import com.android.contacts.util.StructuredPostalUtils; 173import com.android.contacts.util.UriUtils; 174import com.android.contacts.util.ViewUtil; 175import com.android.contacts.widget.MultiShrinkScroller; 176import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; 177import com.android.contacts.widget.QuickContactImageView; 178import com.android.contactsbind.HelpUtils; 179 180import com.google.common.collect.Lists; 181 182import java.util.ArrayList; 183import java.util.Arrays; 184import java.util.Calendar; 185import java.util.Collections; 186import java.util.Comparator; 187import java.util.Date; 188import java.util.HashMap; 189import java.util.List; 190import java.util.Map; 191import java.util.concurrent.ConcurrentHashMap; 192 193/** 194 * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads 195 * data asynchronously, and then shows a popup with details centered around 196 * {@link Intent#getSourceBounds()}. 197 */ 198public class QuickContactActivity extends ContactsActivity { 199 200 /** 201 * QuickContacts immediately takes up the full screen. All possible information is shown. 202 * This value for {@link android.provider.ContactsContract.QuickContact#EXTRA_MODE} 203 * should only be used by the Contacts app. 204 */ 205 public static final int MODE_FULLY_EXPANDED = 4; 206 207 /** Used to pass the screen where the user came before launching this Activity. */ 208 public static final String EXTRA_PREVIOUS_SCREEN_TYPE = "previous_screen_type"; 209 /** Used to pass the Contact card action. */ 210 public static final String EXTRA_ACTION_TYPE = "action_type"; 211 public static final String EXTRA_THIRD_PARTY_ACTION = "third_party_action"; 212 213 /** Used to tell the QuickContact that the previous contact was edited, so it can return an 214 * activity result back to the original Activity that launched it. */ 215 public static final String EXTRA_CONTACT_EDITED = "contact_edited"; 216 217 private static final String TAG = "QuickContact"; 218 219 private static final String KEY_THEME_COLOR = "theme_color"; 220 private static final String KEY_PREVIOUS_CONTACT_ID = "previous_contact_id"; 221 222 private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState"; 223 private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable"; 224 private static final String KEY_CUSTOM_RINGTONE = "customRingtone"; 225 226 private static final int ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION = 150; 227 private static final int REQUEST_CODE_CONTACT_EDITOR_ACTIVITY = 1; 228 private static final int SCRIM_COLOR = Color.argb(0xC8, 0, 0, 0); 229 private static final int REQUEST_CODE_CONTACT_SELECTION_ACTIVITY = 2; 230 private static final String MIMETYPE_SMS = "vnd.android-dir/mms-sms"; 231 private static final int REQUEST_CODE_JOIN = 3; 232 private static final int REQUEST_CODE_PICK_RINGTONE = 4; 233 234 private static final int CURRENT_API_VERSION = android.os.Build.VERSION.SDK_INT; 235 236 /** This is the Intent action to install a shortcut in the launcher. */ 237 private static final String ACTION_INSTALL_SHORTCUT = 238 "com.android.launcher.action.INSTALL_SHORTCUT"; 239 240 public static final String ACTION_SPLIT_COMPLETED = "splitCompleted"; 241 242 // Phone specific option menu items 243 private boolean mSendToVoicemailState; 244 private boolean mArePhoneOptionsChangable; 245 private String mCustomRingtone; 246 247 @SuppressWarnings("deprecation") 248 private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; 249 250 public static final String MIMETYPE_TACHYON = 251 "vnd.android.cursor.item/com.google.android.apps.tachyon.phone"; 252 private static final String TACHYON_CALL_ACTION = 253 "com.google.android.apps.tachyon.action.CALL"; 254 private static final String MIMETYPE_GPLUS_PROFILE = 255 "vnd.android.cursor.item/vnd.googleplus.profile"; 256 private static final String GPLUS_PROFILE_DATA_5_VIEW_PROFILE = "view"; 257 private static final String MIMETYPE_HANGOUTS = 258 "vnd.android.cursor.item/vnd.googleplus.profile.comm"; 259 private static final String HANGOUTS_DATA_5_VIDEO = "hangout"; 260 private static final String HANGOUTS_DATA_5_MESSAGE = "conversation"; 261 private static final String CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY = 262 "com.android.contacts.quickcontact.QuickContactActivity"; 263 264 // Set true in {@link #onCreate} after orientation change for later use in processIntent(). 265 private boolean mIsRecreatedInstance; 266 private boolean mShortcutUsageReported = false; 267 268 private boolean mShouldLog; 269 270 // Used to store and log the referrer package name and the contact type. 271 private String mReferrer; 272 private int mContactType; 273 274 /** 275 * The URI used to load the the Contact. Once the contact is loaded, use Contact#getLookupUri() 276 * instead of referencing this URI. 277 */ 278 private Uri mLookupUri; 279 private String[] mExcludeMimes; 280 private int mExtraMode; 281 private String mExtraPrioritizedMimeType; 282 private int mStatusBarColor; 283 private boolean mHasAlreadyBeenOpened; 284 private boolean mOnlyOnePhoneNumber; 285 private boolean mOnlyOneEmail; 286 private ProgressDialog mProgressDialog; 287 private SaveServiceListener mListener; 288 289 private QuickContactImageView mPhotoView; 290 private ExpandingEntryCardView mContactCard; 291 private ExpandingEntryCardView mNoContactDetailsCard; 292 private ExpandingEntryCardView mRecentCard; 293 private ExpandingEntryCardView mAboutCard; 294 private ExpandingEntryCardView mPermissionExplanationCard; 295 296 private long mPreviousContactId = 0; 297 // Permission explanation card. 298 private boolean mShouldShowPermissionExplanation = false; 299 private String mPermissionExplanationCardSubHeader = ""; 300 301 private MultiShrinkScroller mScroller; 302 private AsyncTask<Void, Void, Cp2DataCardModel> mEntriesAndActionsTask; 303 private AsyncTask<Void, Void, Void> mRecentDataTask; 304 305 /** 306 * The last copy of Cp2DataCardModel that was passed to {@link #populateContactAndAboutCard}. 307 */ 308 private Cp2DataCardModel mCachedCp2DataCardModel; 309 /** 310 * This scrim's opacity is controlled in two different ways. 1) Before the initial entrance 311 * animation finishes, the opacity is animated by a value animator. This is designed to 312 * distract the user from the length of the initial loading time. 2) After the initial 313 * entrance animation, the opacity is directly related to scroll position. 314 */ 315 private ColorDrawable mWindowScrim; 316 private boolean mIsEntranceAnimationFinished; 317 private MaterialColorMapUtils mMaterialColorMapUtils; 318 private boolean mIsExitAnimationInProgress; 319 private boolean mHasComputedThemeColor; 320 321 /** 322 * Used to stop the ExpandingEntry cards from adjusting between an entry click and the intent 323 * being launched. 324 */ 325 private boolean mHasIntentLaunched; 326 327 private Contact mContactData; 328 private ContactLoader mContactLoader; 329 private PorterDuffColorFilter mColorFilter; 330 private int mColorFilterColor; 331 332 private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); 333 334 /** 335 * {@link #LEADING_MIMETYPES} is used to sort MIME-types. 336 * 337 * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, 338 * in the order specified here.</p> 339 */ 340 private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( 341 Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE, 342 StructuredPostal.CONTENT_ITEM_TYPE); 343 344 private static final List<String> SORTED_ABOUT_CARD_MIMETYPES = Lists.newArrayList( 345 Nickname.CONTENT_ITEM_TYPE, 346 // Phonetic name is inserted after nickname if it is available. 347 // No mimetype for phonetic name exists. 348 Website.CONTENT_ITEM_TYPE, 349 Organization.CONTENT_ITEM_TYPE, 350 Event.CONTENT_ITEM_TYPE, 351 Relation.CONTENT_ITEM_TYPE, 352 Im.CONTENT_ITEM_TYPE, 353 GroupMembership.CONTENT_ITEM_TYPE, 354 Identity.CONTENT_ITEM_TYPE, 355 CustomDataItem.MIMETYPE_CUSTOM_FIELD, 356 Note.CONTENT_ITEM_TYPE); 357 358 private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance(); 359 360 /** Id for the background contact loader */ 361 private static final int LOADER_CONTACT_ID = 0; 362 363 /** Id for the background Sms Loader */ 364 private static final int LOADER_SMS_ID = 1; 365 private static final int MAX_SMS_RETRIEVE = 3; 366 367 /** Id for the back Calendar Loader */ 368 private static final int LOADER_CALENDAR_ID = 2; 369 private static final String KEY_LOADER_EXTRA_EMAILS = 370 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_EMAILS"; 371 private static final int MAX_PAST_CALENDAR_RETRIEVE = 3; 372 private static final int MAX_FUTURE_CALENDAR_RETRIEVE = 3; 373 private static final long PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 374 1L * 24L * 60L * 60L * 1000L /* 1 day */; 375 private static final long FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR = 376 7L * 24L * 60L * 60L * 1000L /* 7 days */; 377 378 /** Id for the background Call Log Loader */ 379 private static final int LOADER_CALL_LOG_ID = 3; 380 private static final int MAX_CALL_LOG_RETRIEVE = 3; 381 private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3; 382 private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3; 383 private static final int CARD_ENTRY_ID_EDIT_CONTACT = -2; 384 private static final int CARD_ENTRY_ID_REQUEST_PERMISSION = -3; 385 private static final String KEY_LOADER_EXTRA_PHONES = 386 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_PHONES"; 387 private static final String KEY_LOADER_EXTRA_SIP_NUMBERS = 388 QuickContactActivity.class.getCanonicalName() + ".KEY_LOADER_EXTRA_SIP_NUMBERS"; 389 390 private static final int[] mRecentLoaderIds = new int[]{ 391 LOADER_SMS_ID, 392 LOADER_CALENDAR_ID, 393 LOADER_CALL_LOG_ID}; 394 /** 395 * ConcurrentHashMap constructor params: 4 is initial table size, 0.9f is 396 * load factor before resizing, 1 means we only expect a single thread to 397 * write to the map so make only a single shard 398 */ 399 private Map<Integer, List<ContactInteraction>> mRecentLoaderResults = 400 new ConcurrentHashMap<>(4, 0.9f, 1); 401 402 private static final String FRAGMENT_TAG_SELECT_ACCOUNT = "select_account_fragment"; 403 404 final OnClickListener mEntryClickHandler = new OnClickListener() { 405 @Override 406 public void onClick(View v) { 407 final Object entryTagObject = v.getTag(); 408 if (entryTagObject == null || !(entryTagObject instanceof EntryTag)) { 409 Log.w(TAG, "EntryTag was not used correctly"); 410 return; 411 } 412 final EntryTag entryTag = (EntryTag) entryTagObject; 413 final Intent intent = entryTag.getIntent(); 414 final int dataId = entryTag.getId(); 415 416 if (dataId == CARD_ENTRY_ID_EDIT_CONTACT) { 417 editContact(); 418 return; 419 } 420 421 if (dataId == CARD_ENTRY_ID_REQUEST_PERMISSION) { 422 finish(); 423 RequestDesiredPermissionsActivity.startPermissionActivity( 424 QuickContactActivity.this); 425 return; 426 } 427 428 // Pass the touch point through the intent for use in the InCallUI 429 if (Intent.ACTION_CALL.equals(intent.getAction())) { 430 if (TouchPointManager.getInstance().hasValidPoint()) { 431 Bundle extras = new Bundle(); 432 extras.putParcelable(TouchPointManager.TOUCH_POINT, 433 TouchPointManager.getInstance().getPoint()); 434 intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras); 435 } 436 } 437 438 mHasIntentLaunched = true; 439 try { 440 final int actionType = intent.getIntExtra(EXTRA_ACTION_TYPE, 441 ActionType.UNKNOWN_ACTION); 442 final String thirdPartyAction = intent.getStringExtra(EXTRA_THIRD_PARTY_ACTION); 443 Logger.logQuickContactEvent(mReferrer, mContactType, 444 CardType.UNKNOWN_CARD, actionType, thirdPartyAction); 445 // For the tachyon call action, we need to use startActivityForResult and not 446 // add FLAG_ACTIVITY_NEW_TASK to the intent. 447 if (TACHYON_CALL_ACTION.equals(intent.getAction())) { 448 QuickContactActivity.this.startActivityForResult(intent, /* requestCode */ 0); 449 } else { 450 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 451 ImplicitIntentsUtil.startActivityInAppIfPossible(QuickContactActivity.this, 452 intent); 453 } 454 } catch (SecurityException ex) { 455 Toast.makeText(QuickContactActivity.this, R.string.missing_app, 456 Toast.LENGTH_SHORT).show(); 457 Log.e(TAG, "QuickContacts does not have permission to launch " 458 + intent); 459 } catch (ActivityNotFoundException ex) { 460 Toast.makeText(QuickContactActivity.this, R.string.missing_app, 461 Toast.LENGTH_SHORT).show(); 462 } 463 464 // Default to USAGE_TYPE_CALL. Usage is summed among all types for sorting each data id 465 // so the exact usage type is not necessary in all cases 466 String usageType = DataUsageFeedback.USAGE_TYPE_CALL; 467 468 final Uri intentUri = intent.getData(); 469 if ((intentUri != null && intentUri.getScheme() != null && 470 intentUri.getScheme().equals(ContactsUtils.SCHEME_SMSTO)) || 471 (intent.getType() != null && intent.getType().equals(MIMETYPE_SMS))) { 472 usageType = DataUsageFeedback.USAGE_TYPE_SHORT_TEXT; 473 } 474 475 // Data IDs start at 1 so anything less is invalid 476 if (dataId > 0) { 477 final Uri dataUsageUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() 478 .appendPath(String.valueOf(dataId)) 479 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType) 480 .build(); 481 try { 482 final boolean successful = getContentResolver().update( 483 dataUsageUri, new ContentValues(), null, null) > 0; 484 if (!successful) { 485 Log.w(TAG, "DataUsageFeedback increment failed"); 486 } 487 } catch (SecurityException ex) { 488 Log.w(TAG, "DataUsageFeedback increment failed", ex); 489 } 490 } else { 491 Log.w(TAG, "Invalid Data ID"); 492 } 493 } 494 }; 495 496 final ExpandingEntryCardViewListener mExpandingEntryCardViewListener 497 = new ExpandingEntryCardViewListener() { 498 @Override 499 public void onCollapse(int heightDelta) { 500 mScroller.prepareForShrinkingScrollChild(heightDelta); 501 } 502 503 @Override 504 public void onExpand() { 505 mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ true); 506 } 507 508 @Override 509 public void onExpandDone() { 510 mScroller.setDisableTouchesForSuppressLayout(/* areTouchesDisabled = */ false); 511 } 512 }; 513 514 private interface ContextMenuIds { 515 static final int COPY_TEXT = 0; 516 static final int CLEAR_DEFAULT = 1; 517 static final int SET_DEFAULT = 2; 518 } 519 520 private final OnCreateContextMenuListener mEntryContextMenuListener = 521 new OnCreateContextMenuListener() { 522 @Override 523 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 524 if (menuInfo == null) { 525 return; 526 } 527 final EntryContextMenuInfo info = (EntryContextMenuInfo) menuInfo; 528 menu.setHeaderTitle(info.getCopyText()); 529 menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT, 530 ContextMenu.NONE, getString(R.string.copy_text)); 531 532 // Don't allow setting or clearing of defaults for non-editable contacts 533 if (!isContactEditable()) { 534 return; 535 } 536 537 final String selectedMimeType = info.getMimeType(); 538 539 // Defaults to true will only enable the detail to be copied to the clipboard. 540 boolean onlyOneOfMimeType = true; 541 542 // Only allow primary support for Phone and Email content types 543 if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 544 onlyOneOfMimeType = mOnlyOnePhoneNumber; 545 } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 546 onlyOneOfMimeType = mOnlyOneEmail; 547 } 548 549 // Checking for previously set default 550 if (info.isSuperPrimary()) { 551 menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT, 552 ContextMenu.NONE, getString(R.string.clear_default)); 553 } else if (!onlyOneOfMimeType) { 554 menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT, 555 ContextMenu.NONE, getString(R.string.set_default)); 556 } 557 } 558 }; 559 560 @Override 561 public boolean onContextItemSelected(MenuItem item) { 562 EntryContextMenuInfo menuInfo; 563 try { 564 menuInfo = (EntryContextMenuInfo) item.getMenuInfo(); 565 } catch (ClassCastException e) { 566 Log.e(TAG, "bad menuInfo", e); 567 return false; 568 } 569 570 switch (item.getItemId()) { 571 case ContextMenuIds.COPY_TEXT: 572 ClipboardUtils.copyText(this, menuInfo.getCopyLabel(), menuInfo.getCopyText(), 573 true); 574 return true; 575 case ContextMenuIds.SET_DEFAULT: 576 final Intent setIntent = ContactSaveService.createSetSuperPrimaryIntent(this, 577 menuInfo.getId()); 578 this.startService(setIntent); 579 return true; 580 case ContextMenuIds.CLEAR_DEFAULT: 581 final Intent clearIntent = ContactSaveService.createClearPrimaryIntent(this, 582 menuInfo.getId()); 583 this.startService(clearIntent); 584 return true; 585 default: 586 throw new IllegalArgumentException("Unknown menu option " + item.getItemId()); 587 } 588 } 589 590 final MultiShrinkScrollerListener mMultiShrinkScrollerListener 591 = new MultiShrinkScrollerListener() { 592 @Override 593 public void onScrolledOffBottom() { 594 finish(); 595 } 596 597 @Override 598 public void onEnterFullscreen() { 599 updateStatusBarColor(); 600 } 601 602 @Override 603 public void onExitFullscreen() { 604 updateStatusBarColor(); 605 } 606 607 @Override 608 public void onStartScrollOffBottom() { 609 mIsExitAnimationInProgress = true; 610 } 611 612 @Override 613 public void onEntranceAnimationDone() { 614 mIsEntranceAnimationFinished = true; 615 } 616 617 @Override 618 public void onTransparentViewHeightChange(float ratio) { 619 if (mIsEntranceAnimationFinished) { 620 mWindowScrim.setAlpha((int) (0xFF * ratio)); 621 } 622 } 623 }; 624 625 626 /** 627 * Data items are compared to the same mimetype based off of three qualities: 628 * 1. Super primary 629 * 2. Primary 630 * 3. Times used 631 */ 632 private final Comparator<DataItem> mWithinMimeTypeDataItemComparator = 633 new Comparator<DataItem>() { 634 @Override 635 public int compare(DataItem lhs, DataItem rhs) { 636 if (!lhs.getMimeType().equals(rhs.getMimeType())) { 637 Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " + 638 lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType()); 639 return 0; 640 } 641 642 if (lhs.isSuperPrimary()) { 643 return -1; 644 } else if (rhs.isSuperPrimary()) { 645 return 1; 646 } else if (lhs.isPrimary() && !rhs.isPrimary()) { 647 return -1; 648 } else if (!lhs.isPrimary() && rhs.isPrimary()) { 649 return 1; 650 } else { 651 final int lhsTimesUsed = 652 lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 653 final int rhsTimesUsed = 654 rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 655 656 return rhsTimesUsed - lhsTimesUsed; 657 } 658 } 659 }; 660 661 /** 662 * Sorts among different mimetypes based off: 663 * 1. Whether one of the mimetypes is the prioritized mimetype 664 * 2. Number of times used 665 * 3. Last time used 666 * 4. Statically defined 667 */ 668 private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator = 669 new Comparator<List<DataItem>> () { 670 @Override 671 public int compare(List<DataItem> lhsList, List<DataItem> rhsList) { 672 final DataItem lhs = lhsList.get(0); 673 final DataItem rhs = rhsList.get(0); 674 final String lhsMimeType = lhs.getMimeType(); 675 final String rhsMimeType = rhs.getMimeType(); 676 677 // 1. Whether one of the mimetypes is the prioritized mimetype 678 if (!TextUtils.isEmpty(mExtraPrioritizedMimeType) && !lhsMimeType.equals(rhsMimeType)) { 679 if (rhsMimeType.equals(mExtraPrioritizedMimeType)) { 680 return 1; 681 } 682 if (lhsMimeType.equals(mExtraPrioritizedMimeType)) { 683 return -1; 684 } 685 } 686 687 // 2. Number of times used 688 final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed(); 689 final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed(); 690 final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed; 691 if (timesUsedDifference != 0) { 692 return timesUsedDifference; 693 } 694 695 // 3. Last time used 696 final long lhsLastTimeUsed = 697 lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed(); 698 final long rhsLastTimeUsed = 699 rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed(); 700 final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed; 701 if (lastTimeUsedDifference > 0) { 702 return 1; 703 } else if (lastTimeUsedDifference < 0) { 704 return -1; 705 } 706 707 // 4. Resort to a statically defined mimetype order. 708 if (!lhsMimeType.equals(rhsMimeType)) { 709 for (String mimeType : LEADING_MIMETYPES) { 710 if (lhsMimeType.equals(mimeType)) { 711 return -1; 712 } else if (rhsMimeType.equals(mimeType)) { 713 return 1; 714 } 715 } 716 } 717 return 0; 718 } 719 }; 720 721 @Override 722 public boolean dispatchTouchEvent(MotionEvent ev) { 723 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 724 TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); 725 } 726 return super.dispatchTouchEvent(ev); 727 } 728 729 @Override 730 protected void onCreate(Bundle savedInstanceState) { 731 Trace.beginSection("onCreate()"); 732 super.onCreate(savedInstanceState); 733 734 if (RequestPermissionsActivity.startPermissionActivityIfNeeded(this)) { 735 return; 736 } 737 738 mIsRecreatedInstance = savedInstanceState != null; 739 if (mIsRecreatedInstance) { 740 mPreviousContactId = savedInstanceState.getLong(KEY_PREVIOUS_CONTACT_ID); 741 742 // Phone specific options menus 743 mSendToVoicemailState = savedInstanceState.getBoolean(KEY_SEND_TO_VOICE_MAIL_STATE); 744 mArePhoneOptionsChangable = 745 savedInstanceState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE); 746 mCustomRingtone = savedInstanceState.getString(KEY_CUSTOM_RINGTONE); 747 } 748 mProgressDialog = new ProgressDialog(this); 749 mProgressDialog.setIndeterminate(true); 750 mProgressDialog.setCancelable(false); 751 752 mListener = new SaveServiceListener(); 753 final IntentFilter intentFilter = new IntentFilter(); 754 intentFilter.addAction(ContactSaveService.BROADCAST_LINK_COMPLETE); 755 intentFilter.addAction(ContactSaveService.BROADCAST_UNLINK_COMPLETE); 756 LocalBroadcastManager.getInstance(this).registerReceiver(mListener, 757 intentFilter); 758 759 760 mShouldLog = true; 761 762 // There're 3 states for each permission: 763 // 1. App doesn't have permission, not asked user yet. 764 // 2. App doesn't have permission, user denied it previously. 765 // 3. App has permission. 766 // Permission explanation card is displayed only for case 1. 767 final boolean hasTelephonyFeature = 768 getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); 769 770 final boolean hasCalendarPermission = PermissionsUtil.hasPermission( 771 this, Manifest.permission.READ_CALENDAR); 772 final boolean hasSMSPermission = hasTelephonyFeature 773 && PermissionsUtil.hasPermission(this, Manifest.permission.READ_SMS); 774 775 final boolean wasCalendarPermissionDenied = 776 ActivityCompat.shouldShowRequestPermissionRationale( 777 this, Manifest.permission.READ_CALENDAR); 778 final boolean wasSMSPermissionDenied = 779 hasTelephonyFeature && ActivityCompat.shouldShowRequestPermissionRationale( 780 this, Manifest.permission.READ_SMS); 781 782 final boolean shouldDisplayCalendarMessage = 783 !hasCalendarPermission && !wasCalendarPermissionDenied; 784 final boolean shouldDisplaySMSMessage = 785 hasTelephonyFeature && !hasSMSPermission && !wasSMSPermissionDenied; 786 mShouldShowPermissionExplanation = shouldDisplayCalendarMessage || shouldDisplaySMSMessage; 787 788 if (shouldDisplayCalendarMessage && shouldDisplaySMSMessage) { 789 mPermissionExplanationCardSubHeader = 790 getString(R.string.permission_explanation_subheader_calendar_and_SMS); 791 } else if (shouldDisplayCalendarMessage) { 792 mPermissionExplanationCardSubHeader = 793 getString(R.string.permission_explanation_subheader_calendar); 794 } else if (shouldDisplaySMSMessage) { 795 mPermissionExplanationCardSubHeader = 796 getString(R.string.permission_explanation_subheader_SMS); 797 } 798 799 final int previousScreenType = getIntent().getIntExtra 800 (EXTRA_PREVIOUS_SCREEN_TYPE, ScreenType.UNKNOWN); 801 Logger.logScreenView(this, ScreenType.QUICK_CONTACT, previousScreenType); 802 803 mReferrer = getCallingPackage(); 804 if (mReferrer == null && CompatUtils.isLollipopMr1Compatible() && getReferrer() != null) { 805 mReferrer = getReferrer().getAuthority(); 806 } 807 mContactType = ContactType.UNKNOWN_TYPE; 808 809 if (CompatUtils.isLollipopCompatible()) { 810 getWindow().setStatusBarColor(Color.TRANSPARENT); 811 } 812 813 processIntent(getIntent()); 814 815 // Show QuickContact in front of soft input 816 getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 817 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 818 819 setContentView(R.layout.quickcontact_activity); 820 821 mMaterialColorMapUtils = new MaterialColorMapUtils(getResources()); 822 823 mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); 824 825 mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); 826 mNoContactDetailsCard = (ExpandingEntryCardView) findViewById(R.id.no_contact_data_card); 827 mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card); 828 mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card); 829 mPermissionExplanationCard = 830 (ExpandingEntryCardView) findViewById(R.id.permission_explanation_card); 831 832 mPermissionExplanationCard.setOnClickListener(mEntryClickHandler); 833 mNoContactDetailsCard.setOnClickListener(mEntryClickHandler); 834 mContactCard.setOnClickListener(mEntryClickHandler); 835 mContactCard.setOnCreateContextMenuListener(mEntryContextMenuListener); 836 837 mRecentCard.setOnClickListener(mEntryClickHandler); 838 mRecentCard.setTitle(getResources().getString(R.string.recent_card_title)); 839 840 mAboutCard.setOnClickListener(mEntryClickHandler); 841 mAboutCard.setOnCreateContextMenuListener(mEntryContextMenuListener); 842 843 mPhotoView = (QuickContactImageView) findViewById(R.id.photo); 844 final View transparentView = findViewById(R.id.transparent_view); 845 if (mScroller != null) { 846 transparentView.setOnClickListener(new OnClickListener() { 847 @Override 848 public void onClick(View v) { 849 mScroller.scrollOffBottom(); 850 } 851 }); 852 } 853 854 // Allow a shadow to be shown under the toolbar. 855 ViewUtil.addRectangularOutlineProvider(findViewById(R.id.toolbar_parent), getResources()); 856 857 final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 858 setActionBar(toolbar); 859 getActionBar().setTitle(null); 860 // Put a TextView with a known resource id into the ActionBar. This allows us to easily 861 // find the correct TextView location & size later. 862 toolbar.addView(getLayoutInflater().inflate(R.layout.quickcontact_title_placeholder, null)); 863 864 mHasAlreadyBeenOpened = savedInstanceState != null; 865 mIsEntranceAnimationFinished = mHasAlreadyBeenOpened; 866 mWindowScrim = new ColorDrawable(SCRIM_COLOR); 867 mWindowScrim.setAlpha(0); 868 getWindow().setBackgroundDrawable(mWindowScrim); 869 870 mScroller.initialize(mMultiShrinkScrollerListener, mExtraMode == MODE_FULLY_EXPANDED, 871 /* maximumHeaderTextSize */ -1, 872 /* shouldUpdateNameViewHeight */ true); 873 // mScroller needs to perform asynchronous measurements after initalize(), therefore 874 // we can't mark this as GONE. 875 mScroller.setVisibility(View.INVISIBLE); 876 877 setHeaderNameText(R.string.missing_name); 878 879 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ true, 880 new Runnable() { 881 @Override 882 public void run() { 883 if (!mHasAlreadyBeenOpened) { 884 // The initial scrim opacity must match the scrim opacity that would be 885 // achieved by scrolling to the starting position. 886 final float alphaRatio = mExtraMode == MODE_FULLY_EXPANDED ? 887 1 : mScroller.getStartingTransparentHeightRatio(); 888 final int duration = getResources().getInteger( 889 android.R.integer.config_shortAnimTime); 890 final int desiredAlpha = (int) (0xFF * alphaRatio); 891 ObjectAnimator o = ObjectAnimator.ofInt(mWindowScrim, "alpha", 0, 892 desiredAlpha).setDuration(duration); 893 894 o.start(); 895 } 896 } 897 }); 898 899 if (savedInstanceState != null) { 900 final int color = savedInstanceState.getInt(KEY_THEME_COLOR, 0); 901 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 902 new Runnable() { 903 @Override 904 public void run() { 905 // Need to wait for the pre draw before setting the initial scroll 906 // value. Prior to pre draw all scroll values are invalid. 907 if (mHasAlreadyBeenOpened) { 908 mScroller.setVisibility(View.VISIBLE); 909 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen()); 910 } 911 // Need to wait for pre draw for setting the theme color. Setting the 912 // header tint before the MultiShrinkScroller has been measured will 913 // cause incorrect tinting calculations. 914 if (color != 0) { 915 setThemeColor(mMaterialColorMapUtils 916 .calculatePrimaryAndSecondaryColor(color)); 917 } 918 } 919 }); 920 } 921 922 Trace.endSection(); 923 } 924 925 @Override 926 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 927 final boolean deletedOrSplit = requestCode == REQUEST_CODE_CONTACT_EDITOR_ACTIVITY && 928 (resultCode == ContactDeletionInteraction.RESULT_CODE_DELETED || 929 resultCode == ContactEditorActivity.RESULT_CODE_SPLIT); 930 setResult(resultCode, data); 931 if (deletedOrSplit) { 932 finish(); 933 } else if (requestCode == REQUEST_CODE_CONTACT_SELECTION_ACTIVITY && 934 resultCode != RESULT_CANCELED) { 935 processIntent(data); 936 } else if (requestCode == REQUEST_CODE_JOIN) { 937 // Ignore failed requests 938 if (resultCode != Activity.RESULT_OK) { 939 return; 940 } 941 if (data != null) { 942 joinAggregate(ContentUris.parseId(data.getData())); 943 } 944 } else if (requestCode == REQUEST_CODE_PICK_RINGTONE && data != null) { 945 final Uri pickedUri = data.getParcelableExtra( 946 RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 947 onRingtonePicked(pickedUri); 948 } 949 } 950 951 private void onRingtonePicked(Uri pickedUri) { 952 mCustomRingtone = EditorUiUtils.getRingtoneStringFromUri(pickedUri, CURRENT_API_VERSION); 953 Intent intent = ContactSaveService.createSetRingtone( 954 this, mLookupUri, mCustomRingtone); 955 this.startService(intent); 956 } 957 958 @Override 959 protected void onNewIntent(Intent intent) { 960 super.onNewIntent(intent); 961 mHasAlreadyBeenOpened = true; 962 mIsEntranceAnimationFinished = true; 963 mHasComputedThemeColor = false; 964 processIntent(intent); 965 } 966 967 @Override 968 public void onSaveInstanceState(Bundle savedInstanceState) { 969 super.onSaveInstanceState(savedInstanceState); 970 if (mColorFilter != null) { 971 savedInstanceState.putInt(KEY_THEME_COLOR, mColorFilterColor); 972 } 973 savedInstanceState.putLong(KEY_PREVIOUS_CONTACT_ID, mPreviousContactId); 974 975 // Phone specific options 976 savedInstanceState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState); 977 savedInstanceState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable); 978 savedInstanceState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone); 979 } 980 981 private void processIntent(Intent intent) { 982 if (intent == null) { 983 finish(); 984 return; 985 } 986 if (ACTION_SPLIT_COMPLETED.equals(intent.getAction())) { 987 Toast.makeText(this, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT).show(); 988 finish(); 989 return; 990 } 991 992 Uri lookupUri = intent.getData(); 993 if (intent.getBooleanExtra(EXTRA_CONTACT_EDITED, false)) { 994 setResult(ContactEditorActivity.RESULT_CODE_EDITED); 995 } 996 997 // Check to see whether it comes from the old version. 998 if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { 999 final long rawContactId = ContentUris.parseId(lookupUri); 1000 lookupUri = RawContacts.getContactLookupUri(getContentResolver(), 1001 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 1002 } 1003 mExtraMode = getIntent().getIntExtra(QuickContact.EXTRA_MODE, QuickContact.MODE_LARGE); 1004 if (isMultiWindowOnPhone()) { 1005 mExtraMode = QuickContact.MODE_LARGE; 1006 } 1007 mExtraPrioritizedMimeType = 1008 getIntent().getStringExtra(QuickContact.EXTRA_PRIORITIZED_MIMETYPE); 1009 final Uri oldLookupUri = mLookupUri; 1010 1011 1012 if (lookupUri == null) { 1013 finish(); 1014 return; 1015 } 1016 mLookupUri = lookupUri; 1017 mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); 1018 if (oldLookupUri == null) { 1019 // Should not log if only orientation changes. 1020 mShouldLog = !mIsRecreatedInstance; 1021 mContactLoader = (ContactLoader) getLoaderManager().initLoader( 1022 LOADER_CONTACT_ID, null, mLoaderContactCallbacks); 1023 } else if (oldLookupUri != mLookupUri) { 1024 // Should log when reload happens, regardless of orientation change. 1025 mShouldLog = true; 1026 // After copying a directory contact, the contact URI changes. Therefore, 1027 // we need to reload the new contact. 1028 destroyInteractionLoaders(); 1029 mContactLoader = (ContactLoader) (Loader<?>) getLoaderManager().getLoader( 1030 LOADER_CONTACT_ID); 1031 mContactLoader.setNewLookup(mLookupUri); 1032 mCachedCp2DataCardModel = null; 1033 } 1034 mContactLoader.forceLoad(); 1035 1036 NfcHandler.register(this, mLookupUri); 1037 } 1038 1039 private void destroyInteractionLoaders() { 1040 for (int interactionLoaderId : mRecentLoaderIds) { 1041 getLoaderManager().destroyLoader(interactionLoaderId); 1042 } 1043 } 1044 1045 private void runEntranceAnimation() { 1046 if (mHasAlreadyBeenOpened) { 1047 return; 1048 } 1049 mHasAlreadyBeenOpened = true; 1050 mScroller.scrollUpForEntranceAnimation(/* scrollToCurrentPosition */ !isMultiWindowOnPhone() 1051 && (mExtraMode != MODE_FULLY_EXPANDED)); 1052 } 1053 1054 private boolean isMultiWindowOnPhone() { 1055 return MultiWindowCompat.isInMultiWindowMode(this) && PhoneCapabilityTester.isPhone(this); 1056 } 1057 1058 /** Assign this string to the view if it is not empty. */ 1059 private void setHeaderNameText(int resId) { 1060 if (mScroller != null) { 1061 mScroller.setTitle(getText(resId) == null ? null : getText(resId).toString(), 1062 /* isPhoneNumber= */ false); 1063 } 1064 } 1065 1066 /** Assign this string to the view if it is not empty. */ 1067 private void setHeaderNameText(String value, boolean isPhoneNumber) { 1068 if (!TextUtils.isEmpty(value)) { 1069 if (mScroller != null) { 1070 mScroller.setTitle(value, isPhoneNumber); 1071 } 1072 } 1073 } 1074 1075 /** 1076 * Check if the given MIME-type appears in the list of excluded MIME-types 1077 * that the most-recent caller requested. 1078 */ 1079 private boolean isMimeExcluded(String mimeType) { 1080 if (mExcludeMimes == null) return false; 1081 for (String excludedMime : mExcludeMimes) { 1082 if (TextUtils.equals(excludedMime, mimeType)) { 1083 return true; 1084 } 1085 } 1086 return false; 1087 } 1088 1089 /** 1090 * Handle the result from the ContactLoader 1091 */ 1092 private void bindContactData(final Contact data) { 1093 Trace.beginSection("bindContactData"); 1094 1095 final int actionType = mContactData == null ? ActionType.START : ActionType.UNKNOWN_ACTION; 1096 mContactData = data; 1097 1098 final int newContactType; 1099 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 1100 newContactType = ContactType.DIRECTORY; 1101 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 1102 newContactType = ContactType.INVISIBLE_AND_ADDABLE; 1103 } else if (isContactEditable()) { 1104 newContactType = ContactType.EDITABLE; 1105 } else { 1106 newContactType = ContactType.UNKNOWN_TYPE; 1107 } 1108 if (mShouldLog && mContactType != newContactType) { 1109 Logger.logQuickContactEvent(mReferrer, newContactType, CardType.UNKNOWN_CARD, 1110 actionType, /* thirdPartyAction */ null); 1111 } 1112 mContactType = newContactType; 1113 1114 setStateForPhoneMenuItems(mContactData); 1115 invalidateOptionsMenu(); 1116 1117 Trace.endSection(); 1118 Trace.beginSection("Set display photo & name"); 1119 1120 mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization()); 1121 mPhotoSetter.setupContactPhoto(data, mPhotoView); 1122 extractAndApplyTintFromPhotoViewAsynchronously(); 1123 final String displayName = ContactDisplayUtils.getDisplayName(this, data).toString(); 1124 setHeaderNameText( 1125 displayName, mContactData.getDisplayNameSource() == DisplayNameSources.PHONE); 1126 final String phoneticName = ContactDisplayUtils.getPhoneticName(this, data); 1127 if (mScroller != null) { 1128 // Show phonetic name only when it doesn't equal the display name. 1129 if (!TextUtils.isEmpty(phoneticName) && !phoneticName.equals(displayName)) { 1130 mScroller.setPhoneticName(phoneticName); 1131 } else { 1132 mScroller.setPhoneticNameGone(); 1133 } 1134 } 1135 1136 Trace.endSection(); 1137 1138 mEntriesAndActionsTask = new AsyncTask<Void, Void, Cp2DataCardModel>() { 1139 1140 @Override 1141 protected Cp2DataCardModel doInBackground( 1142 Void... params) { 1143 return generateDataModelFromContact(data); 1144 } 1145 1146 @Override 1147 protected void onPostExecute(Cp2DataCardModel cardDataModel) { 1148 super.onPostExecute(cardDataModel); 1149 // Check that original AsyncTask parameters are still valid and the activity 1150 // is still running before binding to UI. A new intent could invalidate 1151 // the results, for example. 1152 if (data == mContactData && !isCancelled()) { 1153 bindDataToCards(cardDataModel); 1154 showActivity(); 1155 } 1156 } 1157 }; 1158 mEntriesAndActionsTask.execute(); 1159 } 1160 1161 private void bindDataToCards(Cp2DataCardModel cp2DataCardModel) { 1162 startInteractionLoaders(cp2DataCardModel); 1163 populateContactAndAboutCard(cp2DataCardModel, /* shouldAddPhoneticName */ true); 1164 } 1165 1166 private void startInteractionLoaders(Cp2DataCardModel cp2DataCardModel) { 1167 final Map<String, List<DataItem>> dataItemsMap = cp2DataCardModel.dataItemsMap; 1168 final List<DataItem> phoneDataItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE); 1169 final List<DataItem> sipCallDataItems = dataItemsMap.get(SipAddress.CONTENT_ITEM_TYPE); 1170 if (phoneDataItems != null && phoneDataItems.size() == 1) { 1171 mOnlyOnePhoneNumber = true; 1172 } 1173 String[] phoneNumbers = null; 1174 if (phoneDataItems != null) { 1175 phoneNumbers = new String[phoneDataItems.size()]; 1176 for (int i = 0; i < phoneDataItems.size(); ++i) { 1177 phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber(); 1178 } 1179 } 1180 String[] sipNumbers = null; 1181 if (sipCallDataItems != null) { 1182 sipNumbers = new String[sipCallDataItems.size()]; 1183 for (int i = 0; i < sipCallDataItems.size(); ++i) { 1184 sipNumbers[i] = ((SipAddressDataItem) sipCallDataItems.get(i)).getSipAddress(); 1185 } 1186 } 1187 final Bundle phonesExtraBundle = new Bundle(); 1188 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers); 1189 phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_SIP_NUMBERS, sipNumbers); 1190 1191 Trace.beginSection("start sms loader"); 1192 getLoaderManager().initLoader( 1193 LOADER_SMS_ID, 1194 phonesExtraBundle, 1195 mLoaderInteractionsCallbacks); 1196 Trace.endSection(); 1197 1198 Trace.beginSection("start call log loader"); 1199 getLoaderManager().initLoader( 1200 LOADER_CALL_LOG_ID, 1201 phonesExtraBundle, 1202 mLoaderInteractionsCallbacks); 1203 Trace.endSection(); 1204 1205 1206 Trace.beginSection("start calendar loader"); 1207 final List<DataItem> emailDataItems = dataItemsMap.get(Email.CONTENT_ITEM_TYPE); 1208 if (emailDataItems != null && emailDataItems.size() == 1) { 1209 mOnlyOneEmail = true; 1210 } 1211 String[] emailAddresses = null; 1212 if (emailDataItems != null) { 1213 emailAddresses = new String[emailDataItems.size()]; 1214 for (int i = 0; i < emailDataItems.size(); ++i) { 1215 emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress(); 1216 } 1217 } 1218 final Bundle emailsExtraBundle = new Bundle(); 1219 emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses); 1220 getLoaderManager().initLoader( 1221 LOADER_CALENDAR_ID, 1222 emailsExtraBundle, 1223 mLoaderInteractionsCallbacks); 1224 Trace.endSection(); 1225 } 1226 1227 private void showActivity() { 1228 if (mScroller != null) { 1229 mScroller.setVisibility(View.VISIBLE); 1230 SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, 1231 new Runnable() { 1232 @Override 1233 public void run() { 1234 runEntranceAnimation(); 1235 } 1236 }); 1237 } 1238 } 1239 1240 private List<List<Entry>> buildAboutCardEntries(Map<String, List<DataItem>> dataItemsMap) { 1241 final List<List<Entry>> aboutCardEntries = new ArrayList<>(); 1242 for (String mimetype : SORTED_ABOUT_CARD_MIMETYPES) { 1243 final List<DataItem> mimeTypeItems = dataItemsMap.get(mimetype); 1244 if (mimeTypeItems == null) { 1245 continue; 1246 } 1247 // Set aboutCardTitleOut = null, since SORTED_ABOUT_CARD_MIMETYPES doesn't contain 1248 // the name mimetype. 1249 final List<Entry> aboutEntries = dataItemsToEntries(mimeTypeItems, 1250 /* aboutCardTitleOut = */ null); 1251 if (aboutEntries.size() > 0) { 1252 aboutCardEntries.add(aboutEntries); 1253 } 1254 } 1255 return aboutCardEntries; 1256 } 1257 1258 @Override 1259 protected void onResume() { 1260 super.onResume(); 1261 // If returning from a launched activity, repopulate the contact and about card 1262 if (mHasIntentLaunched) { 1263 mHasIntentLaunched = false; 1264 populateContactAndAboutCard(mCachedCp2DataCardModel, /* shouldAddPhoneticName */ false); 1265 } 1266 1267 // When exiting the activity and resuming, we want to force a full reload of all the 1268 // interaction data in case something changed in the background. On screen rotation, 1269 // we don't need to do this. And, mCachedCp2DataCardModel will be null, so we won't. 1270 if (mCachedCp2DataCardModel != null) { 1271 destroyInteractionLoaders(); 1272 startInteractionLoaders(mCachedCp2DataCardModel); 1273 } 1274 maybeShowProgressDialog(); 1275 } 1276 1277 1278 @Override 1279 protected void onPause() { 1280 super.onPause(); 1281 dismissProgressBar(); 1282 } 1283 1284 private void populateContactAndAboutCard(Cp2DataCardModel cp2DataCardModel, 1285 boolean shouldAddPhoneticName) { 1286 mCachedCp2DataCardModel = cp2DataCardModel; 1287 if (mHasIntentLaunched || cp2DataCardModel == null) { 1288 return; 1289 } 1290 Trace.beginSection("bind contact card"); 1291 1292 final List<List<Entry>> contactCardEntries = cp2DataCardModel.contactCardEntries; 1293 final List<List<Entry>> aboutCardEntries = cp2DataCardModel.aboutCardEntries; 1294 final String customAboutCardName = cp2DataCardModel.customAboutCardName; 1295 1296 if (contactCardEntries.size() > 0) { 1297 mContactCard.initialize(contactCardEntries, 1298 /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN, 1299 /* isExpanded = */ mContactCard.isExpanded(), 1300 /* isAlwaysExpanded = */ true, 1301 mExpandingEntryCardViewListener, 1302 mScroller); 1303 if (mContactCard.getVisibility() == View.GONE && mShouldLog) { 1304 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.CONTACT, 1305 ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null); 1306 } 1307 mContactCard.setVisibility(View.VISIBLE); 1308 } else { 1309 mContactCard.setVisibility(View.GONE); 1310 } 1311 Trace.endSection(); 1312 1313 Trace.beginSection("bind about card"); 1314 // Phonetic name is not a data item, so the entry needs to be created separately 1315 // But if mCachedCp2DataCardModel is passed to this method (e.g. returning from editor 1316 // without saving any changes), then it should include phoneticName and the phoneticName 1317 // shouldn't be changed. If this is the case, we shouldn't add it again. b/27459294 1318 final String phoneticName = mContactData.getPhoneticName(); 1319 if (shouldAddPhoneticName && !TextUtils.isEmpty(phoneticName)) { 1320 Entry phoneticEntry = new Entry(/* viewId = */ -1, 1321 /* icon = */ null, 1322 getResources().getString(R.string.name_phonetic), 1323 phoneticName, 1324 /* subHeaderIcon = */ null, 1325 /* text = */ null, 1326 /* textIcon = */ null, 1327 /* primaryContentDescription = */ null, 1328 /* intent = */ null, 1329 /* alternateIcon = */ null, 1330 /* alternateIntent = */ null, 1331 /* alternateContentDescription = */ null, 1332 /* shouldApplyColor = */ false, 1333 /* isEditable = */ false, 1334 /* EntryContextMenuInfo = */ new EntryContextMenuInfo(phoneticName, 1335 getResources().getString(R.string.name_phonetic), 1336 /* mimeType = */ null, /* id = */ -1, /* isPrimary = */ false), 1337 /* thirdIcon = */ null, 1338 /* thirdIntent = */ null, 1339 /* thirdContentDescription = */ null, 1340 /* thirdAction = */ Entry.ACTION_NONE, 1341 /* thirdExtras = */ null, 1342 /* shouldApplyThirdIconColor = */ true, 1343 /* iconResourceId = */ 0); 1344 List<Entry> phoneticList = new ArrayList<>(); 1345 phoneticList.add(phoneticEntry); 1346 // Phonetic name comes after nickname. Check to see if the first entry type is nickname 1347 if (aboutCardEntries.size() > 0 && aboutCardEntries.get(0).get(0).getHeader().equals( 1348 getResources().getString(R.string.header_nickname_entry))) { 1349 aboutCardEntries.add(1, phoneticList); 1350 } else { 1351 aboutCardEntries.add(0, phoneticList); 1352 } 1353 } 1354 1355 if (!TextUtils.isEmpty(customAboutCardName)) { 1356 mAboutCard.setTitle(customAboutCardName); 1357 } 1358 1359 mAboutCard.initialize(aboutCardEntries, 1360 /* numInitialVisibleEntries = */ 1, 1361 /* isExpanded = */ true, 1362 /* isAlwaysExpanded = */ true, 1363 mExpandingEntryCardViewListener, 1364 mScroller); 1365 1366 if (contactCardEntries.size() == 0 && aboutCardEntries.size() == 0) { 1367 initializeNoContactDetailCard(cp2DataCardModel.areAllRawContactsSimAccounts); 1368 } else { 1369 mNoContactDetailsCard.setVisibility(View.GONE); 1370 } 1371 1372 // If the Recent card is already initialized (all recent data is loaded), show the About 1373 // card if it has entries. Otherwise About card visibility will be set in bindRecentData() 1374 if (aboutCardEntries.size() > 0) { 1375 if (mAboutCard.getVisibility() == View.GONE && mShouldLog) { 1376 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.ABOUT, 1377 ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null); 1378 } 1379 if (isAllRecentDataLoaded()) { 1380 mAboutCard.setVisibility(View.VISIBLE); 1381 } 1382 } 1383 Trace.endSection(); 1384 } 1385 1386 /** 1387 * Create a card that shows "Add email" and "Add phone number" entries in grey. 1388 * When contact is a SIM contact, only shows "Add phone number". 1389 */ 1390 private void initializeNoContactDetailCard(boolean areAllRawContactsSimAccounts) { 1391 final Drawable phoneIcon = ResourcesCompat.getDrawable(getResources(), 1392 R.drawable.quantum_ic_phone_vd_theme_24, null).mutate(); 1393 final Entry phonePromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 1394 phoneIcon, getString(R.string.quickcontact_add_phone_number), 1395 /* subHeader = */ null, /* subHeaderIcon = */ null, /* text = */ null, 1396 /* textIcon = */ null, /* primaryContentDescription = */ null, 1397 getEditContactIntent(), 1398 /* alternateIcon = */ null, /* alternateIntent = */ null, 1399 /* alternateContentDescription = */ null, /* shouldApplyColor = */ true, 1400 /* isEditable = */ false, /* EntryContextMenuInfo = */ null, 1401 /* thirdIcon = */ null, /* thirdIntent = */ null, 1402 /* thirdContentDescription = */ null, 1403 /* thirdAction = */ Entry.ACTION_NONE, 1404 /* thirdExtras = */ null, 1405 /* shouldApplyThirdIconColor = */ true, 1406 R.drawable.quantum_ic_phone_vd_theme_24); 1407 1408 final List<List<Entry>> promptEntries = new ArrayList<>(); 1409 promptEntries.add(new ArrayList<Entry>(1)); 1410 promptEntries.get(0).add(phonePromptEntry); 1411 1412 if (!areAllRawContactsSimAccounts) { 1413 final Drawable emailIcon = ResourcesCompat.getDrawable(getResources(), 1414 R.drawable.quantum_ic_email_vd_theme_24, null).mutate(); 1415 final Entry emailPromptEntry = new Entry(CARD_ENTRY_ID_EDIT_CONTACT, 1416 emailIcon, getString(R.string.quickcontact_add_email), /* subHeader = */ null, 1417 /* subHeaderIcon = */ null, 1418 /* text = */ null, /* textIcon = */ null, /* primaryContentDescription = */ null, 1419 getEditContactIntent(), /* alternateIcon = */ null, 1420 /* alternateIntent = */ null, /* alternateContentDescription = */ null, 1421 /* shouldApplyColor = */ true, /* isEditable = */ false, 1422 /* EntryContextMenuInfo = */ null, /* thirdIcon = */ null, 1423 /* thirdIntent = */ null, /* thirdContentDescription = */ null, 1424 /* thirdAction = */ Entry.ACTION_NONE, /* thirdExtras = */ null, 1425 /* shouldApplyThirdIconColor = */ true, 1426 R.drawable.quantum_ic_email_vd_theme_24); 1427 1428 promptEntries.add(new ArrayList<Entry>(1)); 1429 promptEntries.get(1).add(emailPromptEntry); 1430 } 1431 1432 final int subHeaderTextColor = getResources().getColor( 1433 R.color.quickcontact_entry_sub_header_text_color); 1434 final PorterDuffColorFilter greyColorFilter = 1435 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP); 1436 mNoContactDetailsCard.initialize(promptEntries, 2, /* isExpanded = */ true, 1437 /* isAlwaysExpanded = */ true, mExpandingEntryCardViewListener, mScroller); 1438 if (mNoContactDetailsCard.getVisibility() == View.GONE && mShouldLog) { 1439 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.NO_CONTACT, 1440 ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null); 1441 } 1442 mNoContactDetailsCard.setVisibility(View.VISIBLE); 1443 mNoContactDetailsCard.setEntryHeaderColor(subHeaderTextColor); 1444 mNoContactDetailsCard.setColorAndFilter(subHeaderTextColor, greyColorFilter); 1445 } 1446 1447 /** 1448 * Builds the {@link DataItem}s Map out of the Contact. 1449 * @param data The contact to build the data from. 1450 * @return A pair containing a list of data items sorted within mimetype and sorted 1451 * amongst mimetype. The map goes from mimetype string to the sorted list of data items within 1452 * mimetype 1453 */ 1454 private Cp2DataCardModel generateDataModelFromContact( 1455 Contact data) { 1456 Trace.beginSection("Build data items map"); 1457 1458 final Map<String, List<DataItem>> dataItemsMap = new HashMap<>(); 1459 final boolean tachyonEnabled = CallUtil.isTachyonEnabled(this); 1460 1461 for (RawContact rawContact : data.getRawContacts()) { 1462 for (DataItem dataItem : rawContact.getDataItems()) { 1463 dataItem.setRawContactId(rawContact.getId()); 1464 1465 final String mimeType = dataItem.getMimeType(); 1466 if (mimeType == null) continue; 1467 1468 if (!MIMETYPE_TACHYON.equals(mimeType)) { 1469 // Only validate non-Tachyon mimetypes. 1470 final AccountType accountType = rawContact.getAccountType(this); 1471 final DataKind dataKind = AccountTypeManager.getInstance(this) 1472 .getKindOrFallback(accountType, mimeType); 1473 if (dataKind == null) continue; 1474 1475 dataItem.setDataKind(dataKind); 1476 1477 final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this, 1478 dataKind)); 1479 1480 if (isMimeExcluded(mimeType) || !hasData) continue; 1481 } else if (!tachyonEnabled) { 1482 // If tachyon isn't enabled, skip its mimetypes. 1483 continue; 1484 } 1485 1486 List<DataItem> dataItemListByType = dataItemsMap.get(mimeType); 1487 if (dataItemListByType == null) { 1488 dataItemListByType = new ArrayList<>(); 1489 dataItemsMap.put(mimeType, dataItemListByType); 1490 } 1491 dataItemListByType.add(dataItem); 1492 } 1493 } 1494 Trace.endSection(); 1495 bindReachability(dataItemsMap); 1496 1497 Trace.beginSection("sort within mimetypes"); 1498 /* 1499 * Sorting is a multi part step. The end result is to a have a sorted list of the most 1500 * used data items, one per mimetype. Then, within each mimetype, the list of data items 1501 * for that type is also sorted, based off of {super primary, primary, times used} in that 1502 * order. 1503 */ 1504 final List<List<DataItem>> dataItemsList = new ArrayList<>(); 1505 for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) { 1506 // Remove duplicate data items 1507 Collapser.collapseList(mimeTypeDataItems, this); 1508 // Sort within mimetype 1509 Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator); 1510 // Add to the list of data item lists 1511 dataItemsList.add(mimeTypeDataItems); 1512 } 1513 Trace.endSection(); 1514 1515 Trace.beginSection("sort amongst mimetypes"); 1516 // Sort amongst mimetypes to bubble up the top data items for the contact card 1517 Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator); 1518 Trace.endSection(); 1519 1520 Trace.beginSection("cp2 data items to entries"); 1521 1522 final List<List<Entry>> contactCardEntries = new ArrayList<>(); 1523 final List<List<Entry>> aboutCardEntries = buildAboutCardEntries(dataItemsMap); 1524 final MutableString aboutCardName = new MutableString(); 1525 1526 for (int i = 0; i < dataItemsList.size(); ++i) { 1527 final List<DataItem> dataItemsByMimeType = dataItemsList.get(i); 1528 final DataItem topDataItem = dataItemsByMimeType.get(0); 1529 if (SORTED_ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) { 1530 // About card mimetypes are built in buildAboutCardEntries, skip here 1531 continue; 1532 } else { 1533 List<Entry> contactEntries = dataItemsToEntries(dataItemsList.get(i), 1534 aboutCardName); 1535 if (contactEntries.size() > 0) { 1536 contactCardEntries.add(contactEntries); 1537 } 1538 } 1539 } 1540 1541 Trace.endSection(); 1542 1543 final Cp2DataCardModel dataModel = new Cp2DataCardModel(); 1544 dataModel.customAboutCardName = aboutCardName.value; 1545 dataModel.aboutCardEntries = aboutCardEntries; 1546 dataModel.contactCardEntries = contactCardEntries; 1547 dataModel.dataItemsMap = dataItemsMap; 1548 dataModel.areAllRawContactsSimAccounts = data.areAllRawContactsSimAccounts(this); 1549 return dataModel; 1550 } 1551 1552 /** 1553 * Bind the custom data items to each {@link PhoneDataItem} that is Tachyon reachable, the data 1554 * will be needed when creating the {@link Entry} for the {@link PhoneDataItem}. 1555 */ 1556 private void bindReachability(Map<String, List<DataItem>> dataItemsMap) { 1557 final List<DataItem> phoneItems = dataItemsMap.get(Phone.CONTENT_ITEM_TYPE); 1558 final List<DataItem> tachyonItems = dataItemsMap.get(MIMETYPE_TACHYON); 1559 if (phoneItems != null && tachyonItems != null) { 1560 for (DataItem phone : phoneItems) { 1561 if (phone instanceof PhoneDataItem && ((PhoneDataItem) phone).getNumber() != null) { 1562 for (DataItem tachyonItem : tachyonItems) { 1563 if (((PhoneDataItem) phone).getNumber().equals( 1564 tachyonItem.getContentValues().getAsString(Data.DATA1))) { 1565 ((PhoneDataItem) phone).setTachyonReachable(true); 1566 ((PhoneDataItem) phone).setReachableDataItem(tachyonItem); 1567 } 1568 } 1569 } 1570 } 1571 } 1572 } 1573 1574 /** 1575 * Class used to hold the About card and Contact cards' data model that gets generated 1576 * on a background thread. All data is from CP2. 1577 */ 1578 private static class Cp2DataCardModel { 1579 /** 1580 * A map between a mimetype string and the corresponding list of data items. The data items 1581 * are in sorted order using mWithinMimeTypeDataItemComparator. 1582 */ 1583 public Map<String, List<DataItem>> dataItemsMap; 1584 public List<List<Entry>> aboutCardEntries; 1585 public List<List<Entry>> contactCardEntries; 1586 public String customAboutCardName; 1587 public boolean areAllRawContactsSimAccounts; 1588 } 1589 1590 private static class MutableString { 1591 public String value; 1592 } 1593 1594 /** 1595 * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display. 1596 * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned. 1597 * 1598 * This runs on a background thread. This is set as static to avoid accidentally adding 1599 * additional dependencies on unsafe things (like the Activity). 1600 * 1601 * @param dataItem The {@link DataItem} to convert. 1602 * @param secondDataItem A second {@link DataItem} to help build a full entry for some 1603 * mimetypes 1604 * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present. 1605 */ 1606 private static Entry dataItemToEntry(DataItem dataItem, DataItem secondDataItem, 1607 Context context, Contact contactData, 1608 final MutableString aboutCardName) { 1609 Drawable icon = null; 1610 String header = null; 1611 String subHeader = null; 1612 Drawable subHeaderIcon = null; 1613 String text = null; 1614 Drawable textIcon = null; 1615 StringBuilder primaryContentDescription = new StringBuilder(); 1616 Spannable phoneContentDescription = null; 1617 Spannable smsContentDescription = null; 1618 Intent intent = null; 1619 boolean shouldApplyColor = true; 1620 boolean shouldApplyThirdIconColor = true; 1621 Drawable alternateIcon = null; 1622 Intent alternateIntent = null; 1623 StringBuilder alternateContentDescription = new StringBuilder(); 1624 final boolean isEditable = false; 1625 EntryContextMenuInfo entryContextMenuInfo = null; 1626 Drawable thirdIcon = null; 1627 Intent thirdIntent = null; 1628 int thirdAction = Entry.ACTION_NONE; 1629 String thirdContentDescription = null; 1630 Bundle thirdExtras = null; 1631 int iconResourceId = 0; 1632 1633 context = context.getApplicationContext(); 1634 final Resources res = context.getResources(); 1635 DataKind kind = dataItem.getDataKind(); 1636 1637 if (dataItem instanceof ImDataItem) { 1638 final ImDataItem im = (ImDataItem) dataItem; 1639 intent = ContactsUtils.buildImIntent(context, im).first; 1640 final boolean isEmail = im.isCreatedFromEmail(); 1641 final int protocol; 1642 if (!im.isProtocolValid()) { 1643 protocol = Im.PROTOCOL_CUSTOM; 1644 } else { 1645 protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); 1646 } 1647 if (protocol == Im.PROTOCOL_CUSTOM) { 1648 // If the protocol is custom, display the "IM" entry header as well to distinguish 1649 // this entry from other ones 1650 header = res.getString(R.string.header_im_entry); 1651 subHeader = Im.getProtocolLabel(res, protocol, 1652 im.getCustomProtocol()).toString(); 1653 text = im.getData(); 1654 } else { 1655 header = Im.getProtocolLabel(res, protocol, 1656 im.getCustomProtocol()).toString(); 1657 subHeader = im.getData(); 1658 } 1659 entryContextMenuInfo = new EntryContextMenuInfo(im.getData(), header, 1660 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1661 } else if (dataItem instanceof OrganizationDataItem) { 1662 final OrganizationDataItem organization = (OrganizationDataItem) dataItem; 1663 header = res.getString(R.string.header_organization_entry); 1664 subHeader = organization.getCompany(); 1665 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1666 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1667 text = organization.getTitle(); 1668 } else if (dataItem instanceof NicknameDataItem) { 1669 final NicknameDataItem nickname = (NicknameDataItem) dataItem; 1670 // Build nickname entries 1671 final boolean isNameRawContact = 1672 (contactData.getNameRawContactId() == dataItem.getRawContactId()); 1673 1674 final boolean duplicatesTitle = 1675 isNameRawContact 1676 && contactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 1677 1678 if (!duplicatesTitle) { 1679 header = res.getString(R.string.header_nickname_entry); 1680 subHeader = nickname.getName(); 1681 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1682 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1683 } 1684 } else if (dataItem instanceof CustomDataItem) { 1685 final CustomDataItem customDataItem = (CustomDataItem) dataItem; 1686 final String summary = customDataItem.getSummary(); 1687 header = TextUtils.isEmpty(summary) 1688 ? res.getString(R.string.label_custom_field) : summary; 1689 subHeader = customDataItem.getContent(); 1690 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1691 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1692 } else if (dataItem instanceof NoteDataItem) { 1693 final NoteDataItem note = (NoteDataItem) dataItem; 1694 header = res.getString(R.string.header_note_entry); 1695 subHeader = note.getNote(); 1696 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1697 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1698 } else if (dataItem instanceof WebsiteDataItem) { 1699 final WebsiteDataItem website = (WebsiteDataItem) dataItem; 1700 header = res.getString(R.string.header_website_entry); 1701 subHeader = website.getUrl(); 1702 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1703 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1704 try { 1705 final WebAddress webAddress = new WebAddress(website.buildDataStringForDisplay 1706 (context, kind)); 1707 intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString())); 1708 } catch (final ParseException e) { 1709 Log.e(TAG, "Couldn't parse website: " + website.buildDataStringForDisplay( 1710 context, kind)); 1711 } 1712 } else if (dataItem instanceof EventDataItem) { 1713 final EventDataItem event = (EventDataItem) dataItem; 1714 final String dataString = event.buildDataStringForDisplay(context, kind); 1715 final Calendar cal = DateUtils.parseDate(dataString, false); 1716 if (cal != null) { 1717 final Date nextAnniversary = 1718 DateUtils.getNextAnnualDate(cal); 1719 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 1720 builder.appendPath("time"); 1721 ContentUris.appendId(builder, nextAnniversary.getTime()); 1722 intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); 1723 } 1724 header = res.getString(R.string.header_event_entry); 1725 if (event.hasKindTypeColumn(kind)) { 1726 subHeader = EventCompat.getTypeLabel(res, event.getKindTypeColumn(kind), 1727 event.getLabel()).toString(); 1728 } 1729 text = DateUtils.formatDate(context, dataString); 1730 entryContextMenuInfo = new EntryContextMenuInfo(text, header, 1731 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1732 } else if (dataItem instanceof RelationDataItem) { 1733 final RelationDataItem relation = (RelationDataItem) dataItem; 1734 final String dataString = relation.buildDataStringForDisplay(context, kind); 1735 if (!TextUtils.isEmpty(dataString)) { 1736 intent = new Intent(Intent.ACTION_SEARCH); 1737 intent.putExtra(SearchManager.QUERY, dataString); 1738 intent.setType(Contacts.CONTENT_TYPE); 1739 } 1740 header = res.getString(R.string.header_relation_entry); 1741 subHeader = relation.getName(); 1742 entryContextMenuInfo = new EntryContextMenuInfo(subHeader, header, 1743 dataItem.getMimeType(), dataItem.getId(), dataItem.isSuperPrimary()); 1744 if (relation.hasKindTypeColumn(kind)) { 1745 text = Relation.getTypeLabel(res, 1746 relation.getKindTypeColumn(kind), 1747 relation.getLabel()).toString(); 1748 } 1749 } else if (dataItem instanceof PhoneDataItem) { 1750 final PhoneDataItem phone = (PhoneDataItem) dataItem; 1751 String phoneLabel = null; 1752 if (!TextUtils.isEmpty(phone.getNumber())) { 1753 primaryContentDescription.append(res.getString(R.string.call_other)).append(" "); 1754 header = sBidiFormatter.unicodeWrap(phone.buildDataStringForDisplay(context, kind), 1755 TextDirectionHeuristics.LTR); 1756 entryContextMenuInfo = new EntryContextMenuInfo(header, 1757 res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(), 1758 dataItem.getId(), dataItem.isSuperPrimary()); 1759 if (phone.hasKindTypeColumn(kind)) { 1760 final int kindTypeColumn = phone.getKindTypeColumn(kind); 1761 final String label = phone.getLabel(); 1762 phoneLabel = label; 1763 if (kindTypeColumn == Phone.TYPE_CUSTOM && TextUtils.isEmpty(label)) { 1764 text = ""; 1765 } else { 1766 text = Phone.getTypeLabel(res, kindTypeColumn, label).toString(); 1767 phoneLabel= text; 1768 primaryContentDescription.append(text).append(" "); 1769 } 1770 } 1771 primaryContentDescription.append(header); 1772 phoneContentDescription = com.android.contacts.util.ContactDisplayUtils 1773 .getTelephoneTtsSpannable(primaryContentDescription.toString(), header); 1774 iconResourceId = R.drawable.quantum_ic_phone_vd_theme_24; 1775 icon = res.getDrawable(iconResourceId); 1776 if (PhoneCapabilityTester.isPhone(context)) { 1777 intent = CallUtil.getCallIntent(phone.getNumber()); 1778 intent.putExtra(EXTRA_ACTION_TYPE, ActionType.CALL); 1779 } 1780 alternateIntent = new Intent(Intent.ACTION_SENDTO, 1781 Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phone.getNumber(), null)); 1782 alternateIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.SMS); 1783 1784 alternateIcon = res.getDrawable(R.drawable.quantum_ic_message_vd_theme_24); 1785 alternateContentDescription.append(res.getString(R.string.sms_custom, header)); 1786 smsContentDescription = com.android.contacts.util.ContactDisplayUtils 1787 .getTelephoneTtsSpannable(alternateContentDescription.toString(), header); 1788 1789 int videoCapability = CallUtil.getVideoCallingAvailability(context); 1790 boolean isPresenceEnabled = 1791 (videoCapability & CallUtil.VIDEO_CALLING_PRESENCE) != 0; 1792 boolean isVideoEnabled = (videoCapability & CallUtil.VIDEO_CALLING_ENABLED) != 0; 1793 // Check to ensure carrier presence indicates the number supports video calling. 1794 int carrierPresence = dataItem.getCarrierPresence(); 1795 boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; 1796 1797 if (CallUtil.isCallWithSubjectSupported(context)) { 1798 thirdIcon = res.getDrawable(R.drawable.quantum_ic_perm_phone_msg_vd_theme_24); 1799 thirdAction = Entry.ACTION_CALL_WITH_SUBJECT; 1800 thirdContentDescription = 1801 res.getString(R.string.call_with_a_note); 1802 // Create a bundle containing the data the call subject dialog requires. 1803 thirdExtras = new Bundle(); 1804 thirdExtras.putLong(CallSubjectDialog.ARG_PHOTO_ID, 1805 contactData.getPhotoId()); 1806 thirdExtras.putParcelable(CallSubjectDialog.ARG_PHOTO_URI, 1807 UriUtils.parseUriOrNull(contactData.getPhotoUri())); 1808 thirdExtras.putParcelable(CallSubjectDialog.ARG_CONTACT_URI, 1809 contactData.getLookupUri()); 1810 thirdExtras.putString(CallSubjectDialog.ARG_NAME_OR_NUMBER, 1811 contactData.getDisplayName()); 1812 thirdExtras.putBoolean(CallSubjectDialog.ARG_IS_BUSINESS, false); 1813 thirdExtras.putString(CallSubjectDialog.ARG_NUMBER, 1814 phone.getNumber()); 1815 thirdExtras.putString(CallSubjectDialog.ARG_DISPLAY_NUMBER, 1816 phone.getFormattedPhoneNumber()); 1817 thirdExtras.putString(CallSubjectDialog.ARG_NUMBER_LABEL, 1818 phoneLabel); 1819 } else if (isVideoEnabled && (!isPresenceEnabled || isPresent)) { 1820 thirdIcon = res.getDrawable(R.drawable.quantum_ic_videocam_vd_theme_24); 1821 thirdAction = Entry.ACTION_INTENT; 1822 thirdIntent = CallUtil.getVideoCallIntent(phone.getNumber(), 1823 CALL_ORIGIN_QUICK_CONTACTS_ACTIVITY); 1824 thirdIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.VIDEOCALL); 1825 thirdContentDescription = 1826 res.getString(R.string.description_video_call); 1827 } else if (CallUtil.isTachyonEnabled(context) 1828 && ((PhoneDataItem) dataItem).isTachyonReachable()) { 1829 thirdIcon = res.getDrawable(R.drawable.quantum_ic_videocam_vd_theme_24); 1830 thirdAction = Entry.ACTION_INTENT; 1831 thirdIntent = new Intent(TACHYON_CALL_ACTION); 1832 thirdIntent.setData( 1833 Uri.fromParts(PhoneAccount.SCHEME_TEL, phone.getNumber(), null)); 1834 thirdContentDescription = ((PhoneDataItem) dataItem).getReachableDataItem() 1835 .getContentValues().getAsString(Data.DATA2); 1836 } 1837 } 1838 } else if (dataItem instanceof EmailDataItem) { 1839 final EmailDataItem email = (EmailDataItem) dataItem; 1840 final String address = email.getData(); 1841 if (!TextUtils.isEmpty(address)) { 1842 primaryContentDescription.append(res.getString(R.string.email_other)).append(" "); 1843 final Uri mailUri = Uri.fromParts(ContactsUtils.SCHEME_MAILTO, address, null); 1844 intent = new Intent(Intent.ACTION_SENDTO, mailUri); 1845 intent.putExtra(EXTRA_ACTION_TYPE, ActionType.EMAIL); 1846 header = email.getAddress(); 1847 entryContextMenuInfo = new EntryContextMenuInfo(header, 1848 res.getString(R.string.emailLabelsGroup), dataItem.getMimeType(), 1849 dataItem.getId(), dataItem.isSuperPrimary()); 1850 if (email.hasKindTypeColumn(kind)) { 1851 text = Email.getTypeLabel(res, email.getKindTypeColumn(kind), 1852 email.getLabel()).toString(); 1853 primaryContentDescription.append(text).append(" "); 1854 } 1855 primaryContentDescription.append(header); 1856 iconResourceId = R.drawable.quantum_ic_email_vd_theme_24; 1857 icon = res.getDrawable(iconResourceId); 1858 } 1859 } else if (dataItem instanceof StructuredPostalDataItem) { 1860 StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem; 1861 final String postalAddress = postal.getFormattedAddress(); 1862 if (!TextUtils.isEmpty(postalAddress)) { 1863 primaryContentDescription.append(res.getString(R.string.map_other)).append(" "); 1864 intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress); 1865 intent.putExtra(EXTRA_ACTION_TYPE, ActionType.ADDRESS); 1866 header = postal.getFormattedAddress(); 1867 entryContextMenuInfo = new EntryContextMenuInfo(header, 1868 res.getString(R.string.postalLabelsGroup), dataItem.getMimeType(), 1869 dataItem.getId(), dataItem.isSuperPrimary()); 1870 if (postal.hasKindTypeColumn(kind)) { 1871 text = StructuredPostal.getTypeLabel(res, 1872 postal.getKindTypeColumn(kind), postal.getLabel()).toString(); 1873 primaryContentDescription.append(text).append(" "); 1874 } 1875 primaryContentDescription.append(header); 1876 alternateIntent = 1877 StructuredPostalUtils.getViewPostalAddressDirectionsIntent(postalAddress); 1878 alternateIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.DIRECTIONS); 1879 alternateIcon = res.getDrawable(R.drawable.quantum_ic_directions_vd_theme_24); 1880 alternateContentDescription.append(res.getString( 1881 R.string.content_description_directions)).append(" ").append(header); 1882 iconResourceId = R.drawable.quantum_ic_place_vd_theme_24; 1883 icon = res.getDrawable(iconResourceId); 1884 } 1885 } else if (dataItem instanceof SipAddressDataItem) { 1886 final SipAddressDataItem sip = (SipAddressDataItem) dataItem; 1887 final String address = sip.getSipAddress(); 1888 if (!TextUtils.isEmpty(address)) { 1889 primaryContentDescription.append(res.getString(R.string.call_other)).append( 1890 " "); 1891 if (PhoneCapabilityTester.isSipPhone(context)) { 1892 final Uri callUri = Uri.fromParts(PhoneAccount.SCHEME_SIP, address, null); 1893 intent = CallUtil.getCallIntent(callUri); 1894 intent.putExtra(EXTRA_ACTION_TYPE, ActionType.SIPCALL); 1895 } 1896 header = address; 1897 entryContextMenuInfo = new EntryContextMenuInfo(header, 1898 res.getString(R.string.phoneLabelsGroup), dataItem.getMimeType(), 1899 dataItem.getId(), dataItem.isSuperPrimary()); 1900 if (sip.hasKindTypeColumn(kind)) { 1901 text = SipAddress.getTypeLabel(res, 1902 sip.getKindTypeColumn(kind), sip.getLabel()).toString(); 1903 primaryContentDescription.append(text).append(" "); 1904 } 1905 primaryContentDescription.append(header); 1906 iconResourceId = R.drawable.quantum_ic_dialer_sip_vd_theme_24; 1907 icon = res.getDrawable(iconResourceId); 1908 } 1909 } else if (dataItem instanceof StructuredNameDataItem) { 1910 // If the name is already set and this is not the super primary value then leave the 1911 // current value. This way we show the super primary value when we are able to. 1912 if (dataItem.isSuperPrimary() || aboutCardName.value == null 1913 || aboutCardName.value.isEmpty()) { 1914 final String givenName = ((StructuredNameDataItem) dataItem).getGivenName(); 1915 if (!TextUtils.isEmpty(givenName)) { 1916 aboutCardName.value = res.getString(R.string.about_card_title) + 1917 " " + givenName; 1918 } else { 1919 aboutCardName.value = res.getString(R.string.about_card_title); 1920 } 1921 } 1922 } else if (CallUtil.isTachyonEnabled(context) && MIMETYPE_TACHYON.equals( 1923 dataItem.getMimeType())) { 1924 // Skip these actions. They will be placed by the phone number. 1925 return null; 1926 } else { 1927 // Custom DataItem 1928 header = dataItem.buildDataStringForDisplay(context, kind); 1929 text = kind.typeColumn; 1930 intent = new Intent(Intent.ACTION_VIEW); 1931 final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, dataItem.getId()); 1932 intent.setDataAndType(uri, dataItem.getMimeType()); 1933 intent.putExtra(EXTRA_ACTION_TYPE, ActionType.THIRD_PARTY); 1934 intent.putExtra(EXTRA_THIRD_PARTY_ACTION, dataItem.getMimeType()); 1935 1936 if (intent != null) { 1937 final String mimetype = intent.getType(); 1938 // Build advanced entry for known 3p types. Otherwise default to ResolveCache icon. 1939 if (MIMETYPE_HANGOUTS.equals(mimetype)) { 1940 // If a secondDataItem is available, use it to build an entry with 1941 // alternate actions 1942 if (secondDataItem != null) { 1943 icon = res.getDrawable(R.drawable.quantum_ic_hangout_vd_theme_24); 1944 alternateIcon = res.getDrawable( 1945 R.drawable.quantum_ic_hangout_video_vd_theme_24); 1946 final HangoutsDataItemModel itemModel = 1947 new HangoutsDataItemModel(intent, alternateIntent, 1948 dataItem, secondDataItem, alternateContentDescription, 1949 header, text, context); 1950 1951 populateHangoutsDataItemModel(itemModel); 1952 intent = itemModel.intent; 1953 alternateIntent = itemModel.alternateIntent; 1954 alternateContentDescription = itemModel.alternateContentDescription; 1955 header = itemModel.header; 1956 text = itemModel.text; 1957 } else { 1958 if (HANGOUTS_DATA_5_VIDEO.equals(intent.getDataString())) { 1959 icon = res.getDrawable(R.drawable.quantum_ic_hangout_video_vd_theme_24); 1960 } else { 1961 icon = res.getDrawable(R.drawable.quantum_ic_hangout_vd_theme_24); 1962 } 1963 } 1964 } else { 1965 icon = ResolveCache.getInstance(context).getIcon( 1966 dataItem.getMimeType(), intent); 1967 // Call mutate to create a new Drawable.ConstantState for color filtering 1968 if (icon != null) { 1969 icon.mutate(); 1970 } 1971 shouldApplyColor = false; 1972 1973 if (!MIMETYPE_GPLUS_PROFILE.equals(mimetype)) { 1974 entryContextMenuInfo = new EntryContextMenuInfo(header, mimetype, 1975 dataItem.getMimeType(), dataItem.getId(), 1976 dataItem.isSuperPrimary()); 1977 } 1978 } 1979 } 1980 } 1981 1982 if (intent != null) { 1983 // Do not set the intent is there are no resolves 1984 if (!PhoneCapabilityTester.isIntentRegistered(context, intent)) { 1985 intent = null; 1986 } 1987 } 1988 1989 if (alternateIntent != null) { 1990 // Do not set the alternate intent is there are no resolves 1991 if (!PhoneCapabilityTester.isIntentRegistered(context, alternateIntent)) { 1992 alternateIntent = null; 1993 } else if (TextUtils.isEmpty(alternateContentDescription)) { 1994 // Attempt to use package manager to find a suitable content description if needed 1995 alternateContentDescription.append(getIntentResolveLabel(alternateIntent, context)); 1996 } 1997 } 1998 1999 // If the Entry has no visual elements, return null 2000 if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) && 2001 subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) { 2002 return null; 2003 } 2004 2005 // Ignore dataIds from the Me profile. 2006 final int dataId = dataItem.getId() > Integer.MAX_VALUE ? 2007 -1 : (int) dataItem.getId(); 2008 2009 return new Entry(dataId, icon, header, subHeader, subHeaderIcon, text, textIcon, 2010 phoneContentDescription == null 2011 ? new SpannableString(primaryContentDescription.toString()) 2012 : phoneContentDescription, 2013 intent, alternateIcon, alternateIntent, 2014 smsContentDescription == null 2015 ? new SpannableString(alternateContentDescription.toString()) 2016 : smsContentDescription, 2017 shouldApplyColor, isEditable, 2018 entryContextMenuInfo, thirdIcon, thirdIntent, thirdContentDescription, thirdAction, 2019 thirdExtras, shouldApplyThirdIconColor, iconResourceId); 2020 } 2021 2022 private List<Entry> dataItemsToEntries(List<DataItem> dataItems, 2023 MutableString aboutCardTitleOut) { 2024 // Hangouts and G+ use two data items to create one entry. 2025 if (dataItems.get(0).getMimeType().equals(MIMETYPE_GPLUS_PROFILE)) { 2026 return gPlusDataItemsToEntries(dataItems); 2027 } else if (dataItems.get(0).getMimeType().equals(MIMETYPE_HANGOUTS)) { 2028 return hangoutsDataItemsToEntries(dataItems); 2029 } else { 2030 final List<Entry> entries = new ArrayList<>(); 2031 for (DataItem dataItem : dataItems) { 2032 final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null, 2033 this, mContactData, aboutCardTitleOut); 2034 if (entry != null) { 2035 entries.add(entry); 2036 } 2037 } 2038 return entries; 2039 } 2040 } 2041 2042 /** 2043 * Put the data items into buckets based on the raw contact id 2044 */ 2045 private Map<Long, List<DataItem>> dataItemsToBucket(List<DataItem> dataItems) { 2046 final Map<Long, List<DataItem>> buckets = new HashMap<>(); 2047 for (DataItem dataItem : dataItems) { 2048 List<DataItem> bucket = buckets.get(dataItem.getRawContactId()); 2049 if (bucket == null) { 2050 bucket = new ArrayList<>(); 2051 buckets.put(dataItem.getRawContactId(), bucket); 2052 } 2053 bucket.add(dataItem); 2054 } 2055 return buckets; 2056 } 2057 2058 /** 2059 * For G+ entries, a single ExpandingEntryCardView.Entry consists of two data items. This 2060 * method use only the View profile to build entry. 2061 */ 2062 private List<Entry> gPlusDataItemsToEntries(List<DataItem> dataItems) { 2063 final List<Entry> entries = new ArrayList<>(); 2064 2065 for (List<DataItem> bucket : dataItemsToBucket(dataItems).values()) { 2066 for (DataItem dataItem : bucket) { 2067 if (GPLUS_PROFILE_DATA_5_VIEW_PROFILE.equals( 2068 dataItem.getContentValues().getAsString(Data.DATA5))) { 2069 final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null, 2070 this, mContactData, /* aboutCardName = */ null); 2071 if (entry != null) { 2072 entries.add(entry); 2073 } 2074 } 2075 } 2076 } 2077 return entries; 2078 } 2079 2080 /** 2081 * For Hangouts entries, a single ExpandingEntryCardView.Entry consists of two data items. This 2082 * method attempts to build each entry using the two data items if they are available. If there 2083 * are more or less than two data items, a fall back is used and each data item gets its own 2084 * entry. 2085 */ 2086 private List<Entry> hangoutsDataItemsToEntries(List<DataItem> dataItems) { 2087 final List<Entry> entries = new ArrayList<>(); 2088 2089 // Use the buckets to build entries. If a bucket contains two data items, build the special 2090 // entry, otherwise fall back to the normal entry. 2091 for (List<DataItem> bucket : dataItemsToBucket(dataItems).values()) { 2092 if (bucket.size() == 2) { 2093 // Use the pair to build an entry 2094 final Entry entry = dataItemToEntry(bucket.get(0), 2095 /* secondDataItem = */ bucket.get(1), this, mContactData, 2096 /* aboutCardName = */ null); 2097 if (entry != null) { 2098 entries.add(entry); 2099 } 2100 } else { 2101 for (DataItem dataItem : bucket) { 2102 final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null, 2103 this, mContactData, /* aboutCardName = */ null); 2104 if (entry != null) { 2105 entries.add(entry); 2106 } 2107 } 2108 } 2109 } 2110 return entries; 2111 } 2112 2113 /** 2114 * Used for statically passing around Hangouts data items and entry fields to 2115 * populateHangoutsDataItemModel. 2116 */ 2117 private static final class HangoutsDataItemModel { 2118 public Intent intent; 2119 public Intent alternateIntent; 2120 public DataItem dataItem; 2121 public DataItem secondDataItem; 2122 public StringBuilder alternateContentDescription; 2123 public String header; 2124 public String text; 2125 public Context context; 2126 2127 public HangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem, 2128 DataItem secondDataItem, StringBuilder alternateContentDescription, String header, 2129 String text, Context context) { 2130 this.intent = intent; 2131 this.alternateIntent = alternateIntent; 2132 this.dataItem = dataItem; 2133 this.secondDataItem = secondDataItem; 2134 this.alternateContentDescription = alternateContentDescription; 2135 this.header = header; 2136 this.text = text; 2137 this.context = context; 2138 } 2139 } 2140 2141 private static void populateHangoutsDataItemModel( 2142 HangoutsDataItemModel dataModel) { 2143 final Intent secondIntent = new Intent(Intent.ACTION_VIEW); 2144 secondIntent.setDataAndType(ContentUris.withAppendedId(Data.CONTENT_URI, 2145 dataModel.secondDataItem.getId()), dataModel.secondDataItem.getMimeType()); 2146 secondIntent.putExtra(EXTRA_ACTION_TYPE, ActionType.THIRD_PARTY); 2147 secondIntent.putExtra(EXTRA_THIRD_PARTY_ACTION, dataModel.secondDataItem.getMimeType()); 2148 2149 // There is no guarantee the order the data items come in. Second 2150 // data item does not necessarily mean it's the alternate. 2151 // Hangouts video should be alternate. Swap if needed 2152 if (HANGOUTS_DATA_5_VIDEO.equals( 2153 dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) { 2154 dataModel.alternateIntent = dataModel.intent; 2155 dataModel.alternateContentDescription = new StringBuilder(dataModel.header); 2156 2157 dataModel.intent = secondIntent; 2158 dataModel.header = dataModel.secondDataItem.buildDataStringForDisplay( 2159 dataModel.context, dataModel.secondDataItem.getDataKind()); 2160 dataModel.text = dataModel.secondDataItem.getDataKind().typeColumn; 2161 } else if (HANGOUTS_DATA_5_MESSAGE.equals( 2162 dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) { 2163 dataModel.alternateIntent = secondIntent; 2164 dataModel.alternateContentDescription = new StringBuilder( 2165 dataModel.secondDataItem.buildDataStringForDisplay(dataModel.context, 2166 dataModel.secondDataItem.getDataKind())); 2167 } 2168 } 2169 2170 private static String getIntentResolveLabel(Intent intent, Context context) { 2171 final List<ResolveInfo> matches = context.getPackageManager().queryIntentActivities(intent, 2172 PackageManager.MATCH_DEFAULT_ONLY); 2173 2174 // Pick first match, otherwise best found 2175 ResolveInfo bestResolve = null; 2176 final int size = matches.size(); 2177 if (size == 1) { 2178 bestResolve = matches.get(0); 2179 } else if (size > 1) { 2180 bestResolve = ResolveCache.getInstance(context).getBestResolve(intent, matches); 2181 } 2182 2183 if (bestResolve == null) { 2184 return null; 2185 } 2186 2187 return String.valueOf(bestResolve.loadLabel(context.getPackageManager())); 2188 } 2189 2190 /** 2191 * Asynchronously extract the most vibrant color from the PhotoView. Once extracted, 2192 * apply this tint to {@link MultiShrinkScroller}. This operation takes about 20-30ms 2193 * on a Nexus 5. 2194 */ 2195 private void extractAndApplyTintFromPhotoViewAsynchronously() { 2196 if (mScroller == null) { 2197 return; 2198 } 2199 final Drawable imageViewDrawable = mPhotoView.getDrawable(); 2200 new AsyncTask<Void, Void, MaterialPalette>() { 2201 @Override 2202 protected MaterialPalette doInBackground(Void... params) { 2203 2204 if (imageViewDrawable instanceof BitmapDrawable && mContactData != null 2205 && mContactData.getThumbnailPhotoBinaryData() != null 2206 && mContactData.getThumbnailPhotoBinaryData().length > 0) { 2207 // Perform the color analysis on the thumbnail instead of the full sized 2208 // image, so that our results will be as similar as possible to the Bugle 2209 // app. 2210 final Bitmap bitmap = BitmapFactory.decodeByteArray( 2211 mContactData.getThumbnailPhotoBinaryData(), 0, 2212 mContactData.getThumbnailPhotoBinaryData().length); 2213 try { 2214 final int primaryColor = colorFromBitmap(bitmap); 2215 if (primaryColor != 0) { 2216 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor( 2217 primaryColor); 2218 } 2219 } finally { 2220 bitmap.recycle(); 2221 } 2222 } 2223 if (imageViewDrawable instanceof LetterTileDrawable) { 2224 final int primaryColor = ((LetterTileDrawable) imageViewDrawable).getColor(); 2225 return mMaterialColorMapUtils.calculatePrimaryAndSecondaryColor(primaryColor); 2226 } 2227 return MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(getResources()); 2228 } 2229 2230 @Override 2231 protected void onPostExecute(MaterialPalette palette) { 2232 super.onPostExecute(palette); 2233 if (mHasComputedThemeColor) { 2234 // If we had previously computed a theme color from the contact photo, 2235 // then do not update the theme color. Changing the theme color several 2236 // seconds after QC has started, as a result of an updated/upgraded photo, 2237 // is a jarring experience. On the other hand, changing the theme color after 2238 // a rotation or onNewIntent() is perfectly fine. 2239 return; 2240 } 2241 // Check that the Photo has not changed. If it has changed, the new tint 2242 // color needs to be extracted 2243 if (imageViewDrawable == mPhotoView.getDrawable()) { 2244 mHasComputedThemeColor = true; 2245 setThemeColor(palette); 2246 } 2247 } 2248 }.execute(); 2249 } 2250 2251 private void setThemeColor(MaterialPalette palette) { 2252 // If the color is invalid, use the predefined default 2253 mColorFilterColor = palette.mPrimaryColor; 2254 mScroller.setHeaderTintColor(mColorFilterColor); 2255 mStatusBarColor = palette.mSecondaryColor; 2256 updateStatusBarColor(); 2257 2258 mColorFilter = 2259 new PorterDuffColorFilter(mColorFilterColor, PorterDuff.Mode.SRC_ATOP); 2260 mContactCard.setColorAndFilter(mColorFilterColor, mColorFilter); 2261 mRecentCard.setColorAndFilter(mColorFilterColor, mColorFilter); 2262 mAboutCard.setColorAndFilter(mColorFilterColor, mColorFilter); 2263 } 2264 2265 private void updateStatusBarColor() { 2266 if (mScroller == null || !CompatUtils.isLollipopCompatible()) { 2267 return; 2268 } 2269 final int desiredStatusBarColor; 2270 // Only use a custom status bar color if QuickContacts touches the top of the viewport. 2271 if (mScroller.getScrollNeededToBeFullScreen() <= 0) { 2272 desiredStatusBarColor = mStatusBarColor; 2273 } else { 2274 desiredStatusBarColor = Color.TRANSPARENT; 2275 } 2276 // Animate to the new color. 2277 final ObjectAnimator animation = ObjectAnimator.ofInt(getWindow(), "statusBarColor", 2278 getWindow().getStatusBarColor(), desiredStatusBarColor); 2279 animation.setDuration(ANIMATION_STATUS_BAR_COLOR_CHANGE_DURATION); 2280 animation.setEvaluator(new ArgbEvaluator()); 2281 animation.start(); 2282 } 2283 2284 private int colorFromBitmap(Bitmap bitmap) { 2285 // Author of Palette recommends using 24 colors when analyzing profile photos. 2286 final int NUMBER_OF_PALETTE_COLORS = 24; 2287 final Palette palette = Palette.generate(bitmap, NUMBER_OF_PALETTE_COLORS); 2288 if (palette != null && palette.getVibrantSwatch() != null) { 2289 return palette.getVibrantSwatch().getRgb(); 2290 } 2291 return 0; 2292 } 2293 2294 private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) { 2295 final List<Entry> entries = new ArrayList<>(); 2296 for (ContactInteraction interaction : interactions) { 2297 if (interaction == null) { 2298 continue; 2299 } 2300 entries.add(new Entry(/* id = */ -1, 2301 interaction.getIcon(this), 2302 interaction.getViewHeader(this), 2303 interaction.getViewBody(this), 2304 interaction.getBodyIcon(this), 2305 interaction.getViewFooter(this), 2306 interaction.getFooterIcon(this), 2307 interaction.getContentDescription(this), 2308 interaction.getIntent(), 2309 /* alternateIcon = */ null, 2310 /* alternateIntent = */ null, 2311 /* alternateContentDescription = */ null, 2312 /* shouldApplyColor = */ true, 2313 /* isEditable = */ false, 2314 /* EntryContextMenuInfo = */ null, 2315 /* thirdIcon = */ null, 2316 /* thirdIntent = */ null, 2317 /* thirdContentDescription = */ null, 2318 /* thirdAction = */ Entry.ACTION_NONE, 2319 /* thirdActionExtras = */ null, 2320 /* shouldApplyThirdIconColor = */ true, 2321 interaction.getIconResourceId())); 2322 } 2323 return entries; 2324 } 2325 2326 private final LoaderCallbacks<Contact> mLoaderContactCallbacks = 2327 new LoaderCallbacks<Contact>() { 2328 @Override 2329 public void onLoaderReset(Loader<Contact> loader) { 2330 mContactData = null; 2331 } 2332 2333 @Override 2334 public void onLoadFinished(Loader<Contact> loader, Contact data) { 2335 Trace.beginSection("onLoadFinished()"); 2336 try { 2337 2338 if (isFinishing()) { 2339 return; 2340 } 2341 if (data.isError()) { 2342 // This means either the contact is invalid or we had an 2343 // internal error such as an acore crash. 2344 Log.i(TAG, "Failed to load contact: " + ((ContactLoader)loader).getLookupUri()); 2345 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 2346 Toast.LENGTH_LONG).show(); 2347 finish(); 2348 return; 2349 } 2350 if (data.isNotFound()) { 2351 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); 2352 Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, 2353 Toast.LENGTH_LONG).show(); 2354 finish(); 2355 return; 2356 } 2357 2358 if (!mIsRecreatedInstance && !mShortcutUsageReported && data != null) { 2359 mShortcutUsageReported = true; 2360 DynamicShortcuts.reportShortcutUsed(QuickContactActivity.this, 2361 data.getLookupKey()); 2362 } 2363 bindContactData(data); 2364 2365 } finally { 2366 Trace.endSection(); 2367 } 2368 } 2369 2370 @Override 2371 public Loader<Contact> onCreateLoader(int id, Bundle args) { 2372 if (mLookupUri == null) { 2373 Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); 2374 } 2375 // Load all contact data. We need loadGroupMetaData=true to determine whether the 2376 // contact is invisible. If it is, we need to display an "Add to Contacts" MenuItem. 2377 return new ContactLoader(getApplicationContext(), mLookupUri, 2378 true /*loadGroupMetaData*/, true /*postViewNotification*/, 2379 true /*computeFormattedPhoneNumber*/); 2380 } 2381 }; 2382 2383 @Override 2384 public void onBackPressed() { 2385 final int previousScreenType = getIntent().getIntExtra 2386 (EXTRA_PREVIOUS_SCREEN_TYPE, ScreenType.UNKNOWN); 2387 if ((previousScreenType == ScreenType.ALL_CONTACTS 2388 || previousScreenType == ScreenType.FAVORITES) 2389 && !SharedPreferenceUtil.getHamburgerPromoTriggerActionHappenedBefore(this)) { 2390 SharedPreferenceUtil.setHamburgerPromoTriggerActionHappenedBefore(this); 2391 } 2392 if (mScroller != null) { 2393 if (!mIsExitAnimationInProgress) { 2394 mScroller.scrollOffBottom(); 2395 } 2396 } else { 2397 super.onBackPressed(); 2398 } 2399 } 2400 2401 @Override 2402 public void finish() { 2403 super.finish(); 2404 2405 // override transitions to skip the standard window animations 2406 overridePendingTransition(0, 0); 2407 } 2408 2409 private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks = 2410 new LoaderCallbacks<List<ContactInteraction>>() { 2411 2412 @Override 2413 public Loader<List<ContactInteraction>> onCreateLoader(int id, Bundle args) { 2414 Loader<List<ContactInteraction>> loader = null; 2415 switch (id) { 2416 case LOADER_SMS_ID: 2417 loader = new SmsInteractionsLoader( 2418 QuickContactActivity.this, 2419 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 2420 MAX_SMS_RETRIEVE); 2421 break; 2422 case LOADER_CALENDAR_ID: 2423 final String[] emailsArray = args.getStringArray(KEY_LOADER_EXTRA_EMAILS); 2424 List<String> emailsList = null; 2425 if (emailsArray != null) { 2426 emailsList = Arrays.asList(args.getStringArray(KEY_LOADER_EXTRA_EMAILS)); 2427 } 2428 loader = new CalendarInteractionsLoader( 2429 QuickContactActivity.this, 2430 emailsList, 2431 MAX_FUTURE_CALENDAR_RETRIEVE, 2432 MAX_PAST_CALENDAR_RETRIEVE, 2433 FUTURE_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR, 2434 PAST_MILLISECOND_TO_SEARCH_LOCAL_CALENDAR); 2435 break; 2436 case LOADER_CALL_LOG_ID: 2437 loader = new CallLogInteractionsLoader( 2438 QuickContactActivity.this, 2439 args.getStringArray(KEY_LOADER_EXTRA_PHONES), 2440 args.getStringArray(KEY_LOADER_EXTRA_SIP_NUMBERS), 2441 MAX_CALL_LOG_RETRIEVE); 2442 } 2443 return loader; 2444 } 2445 2446 @Override 2447 public void onLoadFinished(Loader<List<ContactInteraction>> loader, 2448 List<ContactInteraction> data) { 2449 mRecentLoaderResults.put(loader.getId(), data); 2450 2451 if (isAllRecentDataLoaded()) { 2452 bindRecentData(); 2453 } 2454 } 2455 2456 @Override 2457 public void onLoaderReset(Loader<List<ContactInteraction>> loader) { 2458 mRecentLoaderResults.remove(loader.getId()); 2459 } 2460 }; 2461 2462 private boolean isAllRecentDataLoaded() { 2463 return mRecentLoaderResults.size() == mRecentLoaderIds.length; 2464 } 2465 2466 private void bindRecentData() { 2467 final List<ContactInteraction> allInteractions = new ArrayList<>(); 2468 final List<List<Entry>> interactionsWrapper = new ArrayList<>(); 2469 2470 // Serialize mRecentLoaderResults into a single list. This should be done on the main 2471 // thread to avoid races against mRecentLoaderResults edits. 2472 for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) { 2473 allInteractions.addAll(loaderInteractions); 2474 } 2475 2476 mRecentDataTask = new AsyncTask<Void, Void, Void>() { 2477 @Override 2478 protected Void doInBackground(Void... params) { 2479 Trace.beginSection("sort recent loader results"); 2480 2481 // Sort the interactions by most recent 2482 Collections.sort(allInteractions, new Comparator<ContactInteraction>() { 2483 @Override 2484 public int compare(ContactInteraction a, ContactInteraction b) { 2485 if (a == null && b == null) { 2486 return 0; 2487 } 2488 if (a == null) { 2489 return 1; 2490 } 2491 if (b == null) { 2492 return -1; 2493 } 2494 if (a.getInteractionDate() > b.getInteractionDate()) { 2495 return -1; 2496 } 2497 if (a.getInteractionDate() == b.getInteractionDate()) { 2498 return 0; 2499 } 2500 return 1; 2501 } 2502 }); 2503 2504 Trace.endSection(); 2505 Trace.beginSection("contactInteractionsToEntries"); 2506 2507 // Wrap each interaction in its own list so that an icon is displayed for each entry 2508 for (Entry contactInteraction : contactInteractionsToEntries(allInteractions)) { 2509 List<Entry> entryListWrapper = new ArrayList<>(1); 2510 entryListWrapper.add(contactInteraction); 2511 interactionsWrapper.add(entryListWrapper); 2512 } 2513 2514 Trace.endSection(); 2515 return null; 2516 } 2517 2518 @Override 2519 protected void onPostExecute(Void aVoid) { 2520 super.onPostExecute(aVoid); 2521 Trace.beginSection("initialize recents card"); 2522 2523 if (allInteractions.size() > 0) { 2524 mRecentCard.initialize(interactionsWrapper, 2525 /* numInitialVisibleEntries = */ MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN, 2526 /* isExpanded = */ mRecentCard.isExpanded(), /* isAlwaysExpanded = */ false, 2527 mExpandingEntryCardViewListener, mScroller); 2528 if (mRecentCard.getVisibility() == View.GONE && mShouldLog) { 2529 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.RECENT, 2530 ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null); 2531 } 2532 mRecentCard.setVisibility(View.VISIBLE); 2533 } else { 2534 mRecentCard.setVisibility(View.GONE); 2535 } 2536 2537 Trace.endSection(); 2538 Trace.beginSection("initialize permission explanation card"); 2539 2540 final Drawable historyIcon = ResourcesCompat.getDrawable(getResources(), 2541 R.drawable.quantum_ic_history_vd_theme_24, null); 2542 2543 final Entry permissionExplanationEntry = new Entry(CARD_ENTRY_ID_REQUEST_PERMISSION, 2544 historyIcon, getString(R.string.permission_explanation_header), 2545 mPermissionExplanationCardSubHeader, /* subHeaderIcon = */ null, 2546 /* text = */ null, /* textIcon = */ null, 2547 /* primaryContentDescription = */ null, getIntent(), 2548 /* alternateIcon = */ null, /* alternateIntent = */ null, 2549 /* alternateContentDescription = */ null, /* shouldApplyColor = */ true, 2550 /* isEditable = */ false, /* EntryContextMenuInfo = */ null, 2551 /* thirdIcon = */ null, /* thirdIntent = */ null, 2552 /* thirdContentDescription = */ null, /* thirdAction = */ Entry.ACTION_NONE, 2553 /* thirdExtras = */ null, 2554 /* shouldApplyThirdIconColor = */ true, 2555 R.drawable.quantum_ic_history_vd_theme_24); 2556 2557 final List<List<Entry>> permissionExplanationEntries = new ArrayList<>(); 2558 permissionExplanationEntries.add(new ArrayList<Entry>()); 2559 permissionExplanationEntries.get(0).add(permissionExplanationEntry); 2560 2561 final int subHeaderTextColor = getResources().getColor(android.R.color.white); 2562 final PorterDuffColorFilter whiteColorFilter = 2563 new PorterDuffColorFilter(subHeaderTextColor, PorterDuff.Mode.SRC_ATOP); 2564 2565 mPermissionExplanationCard.initialize(permissionExplanationEntries, 2566 /* numInitialVisibleEntries = */ 1, 2567 /* isExpanded = */ true, 2568 /* isAlwaysExpanded = */ true, 2569 /* listener = */ null, 2570 mScroller); 2571 2572 mPermissionExplanationCard.setColorAndFilter(subHeaderTextColor, whiteColorFilter); 2573 mPermissionExplanationCard.setBackgroundColor(mColorFilterColor); 2574 mPermissionExplanationCard.setEntryHeaderColor(subHeaderTextColor); 2575 mPermissionExplanationCard.setEntrySubHeaderColor(subHeaderTextColor); 2576 2577 if (mShouldShowPermissionExplanation) { 2578 if (mPermissionExplanationCard.getVisibility() == View.GONE 2579 && mShouldLog) { 2580 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.PERMISSION, 2581 ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null); 2582 } 2583 mPermissionExplanationCard.setVisibility(View.VISIBLE); 2584 } else { 2585 mPermissionExplanationCard.setVisibility(View.GONE); 2586 } 2587 2588 Trace.endSection(); 2589 2590 // About card is initialized along with the contact card, but since it appears after 2591 // the recent card in the UI, we hold off until making it visible until the recent 2592 // card is also ready to avoid stuttering. 2593 if (mAboutCard.shouldShow()) { 2594 mAboutCard.setVisibility(View.VISIBLE); 2595 } else { 2596 mAboutCard.setVisibility(View.GONE); 2597 } 2598 mRecentDataTask = null; 2599 } 2600 }; 2601 mRecentDataTask.execute(); 2602 } 2603 2604 @Override 2605 protected void onStop() { 2606 super.onStop(); 2607 2608 if (mEntriesAndActionsTask != null) { 2609 // Once the activity is stopped, we will no longer want to bind mEntriesAndActionsTask's 2610 // results on the UI thread. In some circumstances Activities are killed without 2611 // onStop() being called. This is not a problem, because in these circumstances 2612 // the entire process will be killed. 2613 mEntriesAndActionsTask.cancel(/* mayInterruptIfRunning = */ false); 2614 } 2615 if (mRecentDataTask != null) { 2616 mRecentDataTask.cancel(/* mayInterruptIfRunning = */ false); 2617 } 2618 } 2619 2620 @Override 2621 public void onDestroy() { 2622 LocalBroadcastManager.getInstance(this).unregisterReceiver(mListener); 2623 super.onDestroy(); 2624 } 2625 2626 /** 2627 * Returns true if it is possible to edit the current contact. 2628 */ 2629 private boolean isContactEditable() { 2630 return mContactData != null && !mContactData.isDirectoryEntry(); 2631 } 2632 2633 /** 2634 * Returns true if it is possible to share the current contact. 2635 */ 2636 private boolean isContactShareable() { 2637 return mContactData != null && !mContactData.isDirectoryEntry(); 2638 } 2639 2640 private Intent getEditContactIntent() { 2641 return EditorIntents.createEditContactIntent(QuickContactActivity.this, 2642 mContactData.getLookupUri(), 2643 mHasComputedThemeColor 2644 ? new MaterialPalette(mColorFilterColor, mStatusBarColor) : null, 2645 mContactData.getPhotoId()); 2646 } 2647 2648 private void editContact() { 2649 mHasIntentLaunched = true; 2650 mContactLoader.cacheResult(); 2651 startActivityForResult(getEditContactIntent(), REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 2652 } 2653 2654 private void deleteContact() { 2655 final Uri contactUri = mContactData.getLookupUri(); 2656 ContactDeletionInteraction.start(this, contactUri, /* finishActivityWhenDone =*/ true); 2657 } 2658 2659 private void toggleStar(MenuItem starredMenuItem, boolean isStarred) { 2660 // To improve responsiveness, swap out the picture (and tag) in the UI already 2661 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem, 2662 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), !isStarred); 2663 2664 // Now perform the real save 2665 final Intent intent = ContactSaveService.createSetStarredIntent( 2666 QuickContactActivity.this, mContactData.getLookupUri(), !isStarred); 2667 startService(intent); 2668 2669 final CharSequence accessibilityText = !isStarred 2670 ? getResources().getText(R.string.description_action_menu_add_star) 2671 : getResources().getText(R.string.description_action_menu_remove_star); 2672 // Accessibility actions need to have an associated view. We can't access the MenuItem's 2673 // underlying view, so put this accessibility action on the root view. 2674 mScroller.announceForAccessibility(accessibilityText); 2675 } 2676 2677 private void shareContact() { 2678 final String lookupKey = mContactData.getLookupKey(); 2679 final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); 2680 final Intent intent = new Intent(Intent.ACTION_SEND); 2681 intent.setType(Contacts.CONTENT_VCARD_TYPE); 2682 intent.putExtra(Intent.EXTRA_STREAM, shareUri); 2683 2684 // Launch chooser to share contact via 2685 final CharSequence chooseTitle = getResources().getQuantityString( 2686 R.plurals.title_share_via, /* quantity */ 1); 2687 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle); 2688 2689 try { 2690 mHasIntentLaunched = true; 2691 ImplicitIntentsUtil.startActivityOutsideApp(this, chooseIntent); 2692 } catch (final ActivityNotFoundException ex) { 2693 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 2694 } 2695 } 2696 2697 /** 2698 * Creates a launcher shortcut with the current contact. 2699 */ 2700 private void createLauncherShortcutWithContact() { 2701 if (BuildCompat.isAtLeastO()) { 2702 final ShortcutManager shortcutManager = (ShortcutManager) 2703 getSystemService(SHORTCUT_SERVICE); 2704 final DynamicShortcuts shortcuts = 2705 new DynamicShortcuts(QuickContactActivity.this); 2706 String displayName = mContactData.getDisplayName(); 2707 if (displayName == null) { 2708 displayName = getString(R.string.missing_name); 2709 } 2710 final ShortcutInfo shortcutInfo = shortcuts.getQuickContactShortcutInfo( 2711 mContactData.getId(), mContactData.getLookupKey(), displayName); 2712 if (shortcutInfo != null) { 2713 shortcutManager.requestPinShortcut(shortcutInfo, null); 2714 } 2715 } else { 2716 final ShortcutIntentBuilder builder = new ShortcutIntentBuilder(this, 2717 new OnShortcutIntentCreatedListener() { 2718 2719 @Override 2720 public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) { 2721 // Broadcast the shortcutIntent to the launcher to create a 2722 // shortcut to this contact 2723 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 2724 QuickContactActivity.this.sendBroadcast(shortcutIntent); 2725 // Send a toast to give feedback to the user that a shortcut to this 2726 // contact was added to the launcher. 2727 final String displayName = shortcutIntent 2728 .getStringExtra(Intent.EXTRA_SHORTCUT_NAME); 2729 final String toastMessage = TextUtils.isEmpty(displayName) 2730 ? getString(R.string.createContactShortcutSuccessful_NoName) 2731 : getString(R.string.createContactShortcutSuccessful, 2732 displayName); 2733 Toast.makeText(QuickContactActivity.this, toastMessage, 2734 Toast.LENGTH_SHORT).show(); 2735 } 2736 }); 2737 builder.createContactShortcutIntent(mContactData.getLookupUri()); 2738 } 2739 } 2740 2741 private boolean isShortcutCreatable() { 2742 if (mContactData == null || mContactData.isUserProfile() || 2743 mContactData.isDirectoryEntry()) { 2744 return false; 2745 } 2746 2747 if (BuildCompat.isAtLeastO()) { 2748 final ShortcutManager manager = (ShortcutManager) 2749 getSystemService(Context.SHORTCUT_SERVICE); 2750 return manager.isRequestPinShortcutSupported(); 2751 } 2752 2753 final Intent createShortcutIntent = new Intent(); 2754 createShortcutIntent.setAction(ACTION_INSTALL_SHORTCUT); 2755 final List<ResolveInfo> receivers = getPackageManager() 2756 .queryBroadcastReceivers(createShortcutIntent, 0); 2757 return receivers != null && receivers.size() > 0; 2758 } 2759 2760 private void setStateForPhoneMenuItems(Contact contact) { 2761 if (contact != null) { 2762 mSendToVoicemailState = contact.isSendToVoicemail(); 2763 mCustomRingtone = contact.getCustomRingtone(); 2764 mArePhoneOptionsChangable = isContactEditable() 2765 && PhoneCapabilityTester.isPhone(this); 2766 } 2767 } 2768 2769 @Override 2770 public boolean onCreateOptionsMenu(Menu menu) { 2771 final MenuInflater inflater = getMenuInflater(); 2772 inflater.inflate(R.menu.quickcontact, menu); 2773 return true; 2774 } 2775 2776 @Override 2777 public boolean onPrepareOptionsMenu(Menu menu) { 2778 if (mContactData != null) { 2779 final MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 2780 ContactDisplayUtils.configureStarredMenuItem(starredMenuItem, 2781 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 2782 mContactData.getStarred()); 2783 2784 // Configure edit MenuItem 2785 final MenuItem editMenuItem = menu.findItem(R.id.menu_edit); 2786 editMenuItem.setVisible(true); 2787 if (DirectoryContactUtil.isDirectoryContact(mContactData) || InvisibleContactUtil 2788 .isInvisibleAndAddable(mContactData, this)) { 2789 editMenuItem.setIcon(R.drawable.quantum_ic_person_add_vd_theme_24); 2790 editMenuItem.setTitle(R.string.menu_add_contact); 2791 } else if (isContactEditable()) { 2792 editMenuItem.setIcon(R.drawable.quantum_ic_create_vd_theme_24); 2793 editMenuItem.setTitle(R.string.menu_editContact); 2794 } else { 2795 editMenuItem.setVisible(false); 2796 } 2797 2798 // The link menu item is only visible if this has a single raw contact. 2799 final MenuItem joinMenuItem = menu.findItem(R.id.menu_join); 2800 joinMenuItem.setVisible(!InvisibleContactUtil.isInvisibleAndAddable(mContactData, this) 2801 && isContactEditable() && !mContactData.isUserProfile() 2802 && !mContactData.isMultipleRawContacts()); 2803 2804 // Viewing linked contacts can only happen if there are multiple raw contacts and 2805 // the link menu isn't available. 2806 final MenuItem linkedContactsMenuItem = menu.findItem(R.id.menu_linked_contacts); 2807 linkedContactsMenuItem.setVisible(mContactData.isMultipleRawContacts() 2808 && !joinMenuItem.isVisible()); 2809 2810 final MenuItem deleteMenuItem = menu.findItem(R.id.menu_delete); 2811 deleteMenuItem.setVisible(isContactEditable() && !mContactData.isUserProfile()); 2812 2813 final MenuItem shareMenuItem = menu.findItem(R.id.menu_share); 2814 shareMenuItem.setVisible(isContactShareable()); 2815 2816 final MenuItem shortcutMenuItem = menu.findItem(R.id.menu_create_contact_shortcut); 2817 shortcutMenuItem.setVisible(isShortcutCreatable()); 2818 2819 // Hide telephony-related settings (ringtone, send to voicemail) 2820 // if we don't have a telephone 2821 final MenuItem ringToneMenuItem = menu.findItem(R.id.menu_set_ringtone); 2822 ringToneMenuItem.setVisible(!mContactData.isUserProfile() && mArePhoneOptionsChangable); 2823 2824 final MenuItem sendToVoiceMailMenuItem = menu.findItem(R.id.menu_send_to_voicemail); 2825 sendToVoiceMailMenuItem.setVisible(!mContactData.isUserProfile() 2826 && mArePhoneOptionsChangable); 2827 sendToVoiceMailMenuItem.setTitle(mSendToVoicemailState 2828 ? R.string.menu_unredirect_calls_to_vm : R.string.menu_redirect_calls_to_vm); 2829 2830 final MenuItem helpMenu = menu.findItem(R.id.menu_help); 2831 helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable()); 2832 2833 return true; 2834 } 2835 return false; 2836 } 2837 2838 @Override 2839 public boolean onOptionsItemSelected(MenuItem item) { 2840 final int id = item.getItemId(); 2841 if (id == R.id.menu_star) {// Make sure there is a contact 2842 if (mContactData != null) { 2843 // Read the current starred value from the UI instead of using the last 2844 // loaded state. This allows rapid tapping without writing the same 2845 // value several times 2846 final boolean isStarred = item.isChecked(); 2847 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2848 isStarred ? ActionType.UNSTAR : ActionType.STAR, 2849 /* thirdPartyAction */ null); 2850 toggleStar(item, isStarred); 2851 } 2852 } else if (id == R.id.menu_edit) { 2853 if (DirectoryContactUtil.isDirectoryContact(mContactData)) { 2854 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2855 ActionType.ADD, /* thirdPartyAction */ null); 2856 2857 // This action is used to launch the contact selector, with the option of 2858 // creating a new contact. Creating a new contact is an INSERT, while selecting 2859 // an exisiting one is an edit. The fields in the edit screen will be 2860 // prepopulated with data. 2861 2862 final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 2863 intent.setType(Contacts.CONTENT_ITEM_TYPE); 2864 2865 ArrayList<ContentValues> values = mContactData.getContentValues(); 2866 2867 // Only pre-fill the name field if the provided display name is an nickname 2868 // or better (e.g. structured name, nickname) 2869 if (mContactData.getDisplayNameSource() >= DisplayNameSources.NICKNAME) { 2870 intent.putExtra(Intents.Insert.NAME, mContactData.getDisplayName()); 2871 } else if (mContactData.getDisplayNameSource() 2872 == DisplayNameSources.ORGANIZATION) { 2873 // This is probably an organization. Instead of copying the organization 2874 // name into a name entry, copy it into the organization entry. This 2875 // way we will still consider the contact an organization. 2876 final ContentValues organization = new ContentValues(); 2877 organization.put(Organization.COMPANY, mContactData.getDisplayName()); 2878 organization.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); 2879 values.add(organization); 2880 } 2881 2882 // Last time used and times used are aggregated values from the usage stat 2883 // table. They need to be removed from data values so the SQL table can insert 2884 // properly 2885 for (ContentValues value : values) { 2886 value.remove(Data.LAST_TIME_USED); 2887 value.remove(Data.TIMES_USED); 2888 } 2889 intent.putExtra(Intents.Insert.DATA, values); 2890 2891 // If the contact can only export to the same account, add it to the intent. 2892 // Otherwise the ContactEditorFragment will show a dialog for selecting 2893 // an account. 2894 if (mContactData.getDirectoryExportSupport() == 2895 Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY) { 2896 intent.putExtra(Intents.Insert.EXTRA_ACCOUNT, 2897 new Account(mContactData.getDirectoryAccountName(), 2898 mContactData.getDirectoryAccountType())); 2899 intent.putExtra(Intents.Insert.EXTRA_DATA_SET, 2900 mContactData.getRawContacts().get(0).getDataSet()); 2901 } 2902 2903 // Add this flag to disable the delete menu option on directory contact joins 2904 // with local contacts. The delete option is ambiguous when joining contacts. 2905 intent.putExtra( 2906 ContactEditorFragment.INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION, 2907 true); 2908 2909 intent.setPackage(getPackageName()); 2910 startActivityForResult(intent, REQUEST_CODE_CONTACT_SELECTION_ACTIVITY); 2911 } else if (InvisibleContactUtil.isInvisibleAndAddable(mContactData, this)) { 2912 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2913 ActionType.ADD, /* thirdPartyAction */ null); 2914 InvisibleContactUtil.addToDefaultGroup(mContactData, this); 2915 } else if (isContactEditable()) { 2916 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2917 ActionType.EDIT, /* thirdPartyAction */ null); 2918 editContact(); 2919 } 2920 } else if (id == R.id.menu_join) { 2921 return doJoinContactAction(); 2922 } else if (id == R.id.menu_linked_contacts) { 2923 return showRawContactPickerDialog(); 2924 } else if (id == R.id.menu_delete) { 2925 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2926 ActionType.REMOVE, /* thirdPartyAction */ null); 2927 if (isContactEditable()) { 2928 deleteContact(); 2929 } 2930 } else if (id == R.id.menu_share) { 2931 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2932 ActionType.SHARE, /* thirdPartyAction */ null); 2933 if (isContactShareable()) { 2934 shareContact(); 2935 } 2936 } else if (id == R.id.menu_create_contact_shortcut) { 2937 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2938 ActionType.SHORTCUT, /* thirdPartyAction */ null); 2939 if (isShortcutCreatable()) { 2940 createLauncherShortcutWithContact(); 2941 } 2942 } else if (id == R.id.menu_set_ringtone) { 2943 doPickRingtone(); 2944 } else if (id == R.id.menu_send_to_voicemail) {// Update state and save 2945 mSendToVoicemailState = !mSendToVoicemailState; 2946 item.setTitle(mSendToVoicemailState 2947 ? R.string.menu_unredirect_calls_to_vm 2948 : R.string.menu_redirect_calls_to_vm); 2949 final Intent intent = ContactSaveService.createSetSendToVoicemail( 2950 this, mLookupUri, mSendToVoicemailState); 2951 this.startService(intent); 2952 } else if (id == R.id.menu_help) { 2953 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2954 ActionType.HELP, /* thirdPartyAction */ null); 2955 HelpUtils.launchHelpAndFeedbackForContactScreen(this); 2956 } else { 2957 Logger.logQuickContactEvent(mReferrer, mContactType, CardType.UNKNOWN_CARD, 2958 ActionType.UNKNOWN_ACTION, /* thirdPartyAction */ null); 2959 return super.onOptionsItemSelected(item); 2960 } 2961 return true; 2962 } 2963 2964 private boolean showRawContactPickerDialog() { 2965 if (mContactData == null) return false; 2966 startActivityForResult(EditorIntents.createViewLinkedContactsIntent( 2967 QuickContactActivity.this, 2968 mContactData.getLookupUri(), 2969 mHasComputedThemeColor 2970 ? new MaterialPalette(mColorFilterColor, mStatusBarColor) 2971 : null), 2972 REQUEST_CODE_CONTACT_EDITOR_ACTIVITY); 2973 return true; 2974 } 2975 2976 private boolean doJoinContactAction() { 2977 if (mContactData == null) return false; 2978 2979 mPreviousContactId = mContactData.getId(); 2980 final Intent intent = new Intent(this, ContactSelectionActivity.class); 2981 intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION); 2982 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mPreviousContactId); 2983 startActivityForResult(intent, REQUEST_CODE_JOIN); 2984 return true; 2985 } 2986 2987 /** 2988 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 2989 */ 2990 private void joinAggregate(final long contactId) { 2991 final Intent intent = ContactSaveService.createJoinContactsIntent( 2992 this, mPreviousContactId, contactId, QuickContactActivity.class, 2993 Intent.ACTION_VIEW); 2994 this.startService(intent); 2995 showLinkProgressBar(); 2996 } 2997 2998 2999 private void doPickRingtone() { 3000 final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 3001 // Allow user to pick 'Default' 3002 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); 3003 // Show only ringtones 3004 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE); 3005 // Allow the user to pick a silent ringtone 3006 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); 3007 3008 final Uri ringtoneUri = EditorUiUtils.getRingtoneUriFromString(mCustomRingtone, 3009 CURRENT_API_VERSION); 3010 3011 // Put checkmark next to the current ringtone for this contact 3012 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri); 3013 3014 // Launch! 3015 try { 3016 startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE); 3017 } catch (ActivityNotFoundException ex) { 3018 Toast.makeText(this, R.string.missing_app, Toast.LENGTH_SHORT).show(); 3019 } 3020 } 3021 3022 private void dismissProgressBar() { 3023 if (mProgressDialog != null && mProgressDialog.isShowing()) { 3024 mProgressDialog.dismiss(); 3025 } 3026 } 3027 3028 private void showLinkProgressBar() { 3029 mProgressDialog.setMessage(getString(R.string.contacts_linking_progress_bar)); 3030 mProgressDialog.show(); 3031 } 3032 3033 private void showUnlinkProgressBar() { 3034 mProgressDialog.setMessage(getString(R.string.contacts_unlinking_progress_bar)); 3035 mProgressDialog.show(); 3036 } 3037 3038 private void maybeShowProgressDialog() { 3039 if (ContactSaveService.getState().isActionPending( 3040 ContactSaveService.ACTION_SPLIT_CONTACT)) { 3041 showUnlinkProgressBar(); 3042 } else if (ContactSaveService.getState().isActionPending( 3043 ContactSaveService.ACTION_JOIN_CONTACTS)) { 3044 showLinkProgressBar(); 3045 } 3046 } 3047 3048 private class SaveServiceListener extends BroadcastReceiver { 3049 @Override 3050 public void onReceive(Context context, Intent intent) { 3051 if (Log.isLoggable(TAG, Log.DEBUG)) { 3052 Log.d(TAG, "Got broadcast from save service " + intent); 3053 } 3054 if (ContactSaveService.BROADCAST_LINK_COMPLETE.equals(intent.getAction()) 3055 || ContactSaveService.BROADCAST_UNLINK_COMPLETE.equals(intent.getAction())) { 3056 dismissProgressBar(); 3057 if (ContactSaveService.BROADCAST_UNLINK_COMPLETE.equals(intent.getAction())) { 3058 finish(); 3059 } 3060 } 3061 } 3062 } 3063} 3064