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