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