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