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