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