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