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