1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.contacts.detail; 18 19 import com.android.contacts.Collapser; 20 import com.android.contacts.Collapser.Collapsible; 21 import com.android.contacts.ContactLoader; 22 import com.android.contacts.ContactPresenceIconUtil; 23 import com.android.contacts.ContactSaveService; 24 import com.android.contacts.ContactsUtils; 25 import com.android.contacts.GroupMetaData; 26 import com.android.contacts.R; 27 import com.android.contacts.TypePrecedence; 28 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener; 29 import com.android.contacts.detail.ContactDetailPhotoSetter; 30 import com.android.contacts.editor.SelectAccountDialogFragment; 31 import com.android.contacts.model.AccountType; 32 import com.android.contacts.model.AccountType.EditType; 33 import com.android.contacts.model.AccountTypeManager; 34 import com.android.contacts.model.AccountWithDataSet; 35 import com.android.contacts.model.DataKind; 36 import com.android.contacts.model.EntityDelta; 37 import com.android.contacts.model.EntityDelta.ValuesDelta; 38 import com.android.contacts.model.EntityDeltaList; 39 import com.android.contacts.model.EntityModifier; 40 import com.android.contacts.util.AccountsListAdapter.AccountListFilter; 41 import com.android.contacts.util.ClipboardUtils; 42 import com.android.contacts.util.Constants; 43 import com.android.contacts.util.DataStatus; 44 import com.android.contacts.util.DateUtils; 45 import com.android.contacts.util.PhoneCapabilityTester; 46 import com.android.contacts.util.StructuredPostalUtils; 47 import com.android.internal.telephony.ITelephony; 48 import com.google.common.annotations.VisibleForTesting; 49 50 import android.app.Activity; 51 import android.app.Fragment; 52 import android.app.SearchManager; 53 import android.content.ContentUris; 54 import android.content.ContentValues; 55 import android.content.Context; 56 import android.content.Entity; 57 import android.content.Entity.NamedContentValues; 58 import android.content.Intent; 59 import android.content.res.Resources; 60 import android.graphics.drawable.Drawable; 61 import android.net.ParseException; 62 import android.net.Uri; 63 import android.net.WebAddress; 64 import android.os.Bundle; 65 import android.os.Parcelable; 66 import android.os.RemoteException; 67 import android.os.ServiceManager; 68 import android.provider.ContactsContract; 69 import android.provider.ContactsContract.CommonDataKinds.Email; 70 import android.provider.ContactsContract.CommonDataKinds.Event; 71 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 72 import android.provider.ContactsContract.CommonDataKinds.Im; 73 import android.provider.ContactsContract.CommonDataKinds.Nickname; 74 import android.provider.ContactsContract.CommonDataKinds.Note; 75 import android.provider.ContactsContract.CommonDataKinds.Organization; 76 import android.provider.ContactsContract.CommonDataKinds.Phone; 77 import android.provider.ContactsContract.CommonDataKinds.Relation; 78 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 79 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 80 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 81 import android.provider.ContactsContract.CommonDataKinds.Website; 82 import android.provider.ContactsContract.Contacts; 83 import android.provider.ContactsContract.Data; 84 import android.provider.ContactsContract.Directory; 85 import android.provider.ContactsContract.DisplayNameSources; 86 import android.provider.ContactsContract.RawContacts; 87 import android.provider.ContactsContract.StatusUpdates; 88 import android.telephony.PhoneNumberUtils; 89 import android.text.TextUtils; 90 import android.util.Log; 91 import android.view.ContextMenu; 92 import android.view.ContextMenu.ContextMenuInfo; 93 import android.view.DragEvent; 94 import android.view.KeyEvent; 95 import android.view.LayoutInflater; 96 import android.view.MenuItem; 97 import android.view.MotionEvent; 98 import android.view.View; 99 import android.view.View.OnClickListener; 100 import android.view.View.OnDragListener; 101 import android.view.View.OnTouchListener; 102 import android.view.ViewGroup; 103 import android.widget.AbsListView.OnScrollListener; 104 import android.widget.AdapterView; 105 import android.widget.AdapterView.AdapterContextMenuInfo; 106 import android.widget.AdapterView.OnItemClickListener; 107 import android.widget.BaseAdapter; 108 import android.widget.Button; 109 import android.widget.ImageView; 110 import android.widget.ListAdapter; 111 import android.widget.ListPopupWindow; 112 import android.widget.ListView; 113 import android.widget.TextView; 114 115 import java.util.ArrayList; 116 import java.util.Collections; 117 import java.util.HashMap; 118 import java.util.List; 119 import java.util.Map; 120 121 public class ContactDetailFragment extends Fragment implements FragmentKeyListener, 122 SelectAccountDialogFragment.Listener, OnItemClickListener { 123 124 private static final String TAG = "ContactDetailFragment"; 125 126 private interface ContextMenuIds { 127 static final int COPY_TEXT = 0; 128 static final int CLEAR_DEFAULT = 1; 129 static final int SET_DEFAULT = 2; 130 } 131 132 private static final String KEY_CONTACT_URI = "contactUri"; 133 private static final String KEY_LIST_STATE = "liststate"; 134 135 private Context mContext; 136 private View mView; 137 private OnScrollListener mVerticalScrollListener; 138 private Uri mLookupUri; 139 private Listener mListener; 140 141 private ContactLoader.Result mContactData; 142 private ViewGroup mStaticPhotoContainer; 143 private View mPhotoTouchOverlay; 144 private ListView mListView; 145 private ViewAdapter mAdapter; 146 private Uri mPrimaryPhoneUri = null; 147 private ViewEntryDimensions mViewEntryDimensions; 148 149 private final ContactDetailPhotoSetter mPhotoSetter = new ContactDetailPhotoSetter(); 150 151 private Button mQuickFixButton; 152 private QuickFix mQuickFix; 153 private String mDefaultCountryIso; 154 private boolean mContactHasSocialUpdates; 155 private boolean mShowStaticPhoto = true; 156 157 private final QuickFix[] mPotentialQuickFixes = new QuickFix[] { 158 new MakeLocalCopyQuickFix(), 159 new AddToMyContactsQuickFix() 160 }; 161 162 /** 163 * Device capability: Set during buildEntries and used in the long-press context menu 164 */ 165 private boolean mHasPhone; 166 167 /** 168 * Device capability: Set during buildEntries and used in the long-press context menu 169 */ 170 private boolean mHasSms; 171 172 /** 173 * Device capability: Set during buildEntries and used in the long-press context menu 174 */ 175 private boolean mHasSip; 176 177 /** 178 * The view shown if the detail list is empty. 179 * We set this to the list view when first bind the adapter, so that it won't be shown while 180 * we're loading data. 181 */ 182 private View mEmptyView; 183 184 /** 185 * Saved state of the {@link ListView}. This must be saved and applied to the {@ListView} only 186 * when the adapter has been populated again. 187 */ 188 private Parcelable mListState; 189 190 /** 191 * A list of distinct contact IDs included in the current contact. 192 */ 193 private ArrayList<Long> mRawContactIds = new ArrayList<Long>(); 194 private ArrayList<DetailViewEntry> mPhoneEntries = new ArrayList<DetailViewEntry>(); 195 private ArrayList<DetailViewEntry> mSmsEntries = new ArrayList<DetailViewEntry>(); 196 private ArrayList<DetailViewEntry> mEmailEntries = new ArrayList<DetailViewEntry>(); 197 private ArrayList<DetailViewEntry> mPostalEntries = new ArrayList<DetailViewEntry>(); 198 private ArrayList<DetailViewEntry> mImEntries = new ArrayList<DetailViewEntry>(); 199 private ArrayList<DetailViewEntry> mNicknameEntries = new ArrayList<DetailViewEntry>(); 200 private ArrayList<DetailViewEntry> mGroupEntries = new ArrayList<DetailViewEntry>(); 201 private ArrayList<DetailViewEntry> mRelationEntries = new ArrayList<DetailViewEntry>(); 202 private ArrayList<DetailViewEntry> mNoteEntries = new ArrayList<DetailViewEntry>(); 203 private ArrayList<DetailViewEntry> mWebsiteEntries = new ArrayList<DetailViewEntry>(); 204 private ArrayList<DetailViewEntry> mSipEntries = new ArrayList<DetailViewEntry>(); 205 private ArrayList<DetailViewEntry> mEventEntries = new ArrayList<DetailViewEntry>(); 206 private final Map<AccountType, List<DetailViewEntry>> mOtherEntriesMap = 207 new HashMap<AccountType, List<DetailViewEntry>>(); 208 private ArrayList<ViewEntry> mAllEntries = new ArrayList<ViewEntry>(); 209 private LayoutInflater mInflater; 210 211 private boolean mIsUniqueNumber; 212 private boolean mIsUniqueEmail; 213 214 private ListPopupWindow mPopup; 215 216 /** 217 * This is to forward touch events to the list view to enable users to scroll the list view 218 * from the blank area underneath the static photo when the layout with static photo is used. 219 */ 220 private OnTouchListener mForwardTouchToListView = new OnTouchListener() { 221 @Override 222 public boolean onTouch(View v, MotionEvent event) { 223 if (mListView != null) { 224 mListView.dispatchTouchEvent(event); 225 return true; 226 } 227 return false; 228 } 229 }; 230 231 /** 232 * This is to forward drag events to the list view to enable users to scroll the list view 233 * from the blank area underneath the static photo when the layout with static photo is used. 234 */ 235 private OnDragListener mForwardDragToListView = new OnDragListener() { 236 @Override 237 public boolean onDrag(View v, DragEvent event) { 238 if (mListView != null) { 239 mListView.dispatchDragEvent(event); 240 return true; 241 } 242 return false; 243 } 244 }; 245 246 public ContactDetailFragment() { 247 // Explicit constructor for inflation 248 } 249 250 @Override 251 public void onCreate(Bundle savedInstanceState) { 252 super.onCreate(savedInstanceState); 253 if (savedInstanceState != null) { 254 mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI); 255 mListState = savedInstanceState.getParcelable(KEY_LIST_STATE); 256 } 257 } 258 259 @Override 260 public void onSaveInstanceState(Bundle outState) { 261 super.onSaveInstanceState(outState); 262 outState.putParcelable(KEY_CONTACT_URI, mLookupUri); 263 if (mListView != null) { 264 outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState()); 265 } 266 } 267 268 @Override 269 public void onPause() { 270 dismissPopupIfShown(); 271 super.onPause(); 272 } 273 274 @Override 275 public void onResume() { 276 super.onResume(); 277 } 278 279 @Override 280 public void onAttach(Activity activity) { 281 super.onAttach(activity); 282 mContext = activity; 283 mDefaultCountryIso = ContactsUtils.getCurrentCountryIso(mContext); 284 mViewEntryDimensions = new ViewEntryDimensions(mContext.getResources()); 285 } 286 287 @Override 288 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 289 mView = inflater.inflate(R.layout.contact_detail_fragment, container, false); 290 // Set the touch and drag listener to forward the event to the mListView so that 291 // vertical scrolling can happen from outside of the list view. 292 mView.setOnTouchListener(mForwardTouchToListView); 293 mView.setOnDragListener(mForwardDragToListView); 294 295 mInflater = inflater; 296 297 mStaticPhotoContainer = (ViewGroup) mView.findViewById(R.id.static_photo_container); 298 mPhotoTouchOverlay = mView.findViewById(R.id.photo_touch_intercept_overlay); 299 300 mListView = (ListView) mView.findViewById(android.R.id.list); 301 mListView.setOnItemClickListener(this); 302 mListView.setItemsCanFocus(true); 303 mListView.setOnScrollListener(mVerticalScrollListener); 304 305 // Don't set it to mListView yet. We do so later when we bind the adapter. 306 mEmptyView = mView.findViewById(android.R.id.empty); 307 308 mQuickFixButton = (Button) mView.findViewById(R.id.contact_quick_fix); 309 mQuickFixButton.setOnClickListener(new OnClickListener() { 310 @Override 311 public void onClick(View v) { 312 if (mQuickFix != null) { 313 mQuickFix.execute(); 314 } 315 } 316 }); 317 318 mView.setVisibility(View.INVISIBLE); 319 320 if (mContactData != null) { 321 bindData(); 322 } 323 324 return mView; 325 } 326 327 public void setListener(Listener value) { 328 mListener = value; 329 } 330 331 protected Context getContext() { 332 return mContext; 333 } 334 335 protected Listener getListener() { 336 return mListener; 337 } 338 339 protected ContactLoader.Result getContactData() { 340 return mContactData; 341 } 342 343 public void setVerticalScrollListener(OnScrollListener listener) { 344 mVerticalScrollListener = listener; 345 } 346 347 public Uri getUri() { 348 return mLookupUri; 349 } 350 351 /** 352 * Sets whether the static contact photo (that is not in a scrolling region), should be shown 353 * or not. 354 */ 355 public void setShowStaticPhoto(boolean showPhoto) { 356 mShowStaticPhoto = showPhoto; 357 } 358 359 /** 360 * Shows the contact detail with a message indicating there are no contact details. 361 */ 362 public void showEmptyState() { 363 setData(null, null); 364 } 365 366 public void setData(Uri lookupUri, ContactLoader.Result result) { 367 mLookupUri = lookupUri; 368 mContactData = result; 369 bindData(); 370 } 371 372 /** 373 * Reset the list adapter in this {@link Fragment} to get rid of any saved scroll position 374 * from a previous contact. 375 */ 376 public void resetAdapter() { 377 if (mListView != null) { 378 mListView.setAdapter(mAdapter); 379 } 380 } 381 382 /** 383 * Returns the top coordinate of the first item in the {@link ListView}. If the first item 384 * in the {@link ListView} is not visible or there are no children in the list, then return 385 * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the 386 * list cannot have a positive offset. 387 */ 388 public int getFirstListItemOffset() { 389 return ContactDetailDisplayUtils.getFirstListItemOffset(mListView); 390 } 391 392 /** 393 * Tries to scroll the first item to the given offset (this can be a no-op if the list is 394 * already in the correct position). 395 * @param offset which should be <= 0 396 */ 397 public void requestToMoveToOffset(int offset) { 398 ContactDetailDisplayUtils.requestToMoveToOffset(mListView, offset); 399 } 400 401 protected void bindData() { 402 if (mView == null) { 403 return; 404 } 405 406 if (isAdded()) { 407 getActivity().invalidateOptionsMenu(); 408 } 409 410 if (mContactData == null) { 411 mView.setVisibility(View.INVISIBLE); 412 if (mStaticPhotoContainer != null) { 413 mStaticPhotoContainer.setVisibility(View.GONE); 414 } 415 mAllEntries.clear(); 416 if (mAdapter != null) { 417 mAdapter.notifyDataSetChanged(); 418 } 419 return; 420 } 421 422 // Figure out if the contact has social updates or not 423 mContactHasSocialUpdates = !mContactData.getStreamItems().isEmpty(); 424 425 // Setup the photo if applicable 426 if (mStaticPhotoContainer != null) { 427 // The presence of a static photo container is not sufficient to determine whether or 428 // not we should show the photo. Check the mShowStaticPhoto flag which can be set by an 429 // outside class depending on screen size, layout, and whether the contact has social 430 // updates or not. 431 if (mShowStaticPhoto) { 432 mStaticPhotoContainer.setVisibility(View.VISIBLE); 433 final ImageView photoView = (ImageView) mStaticPhotoContainer.findViewById( 434 R.id.photo); 435 final boolean expandPhotoOnClick = mContactData.getPhotoUri() != null; 436 final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick( 437 mContext, mContactData, photoView, expandPhotoOnClick); 438 if (mPhotoTouchOverlay != null) { 439 mPhotoTouchOverlay.setVisibility(View.VISIBLE); 440 if (expandPhotoOnClick || mContactData.isWritableContact(mContext)) { 441 mPhotoTouchOverlay.setOnClickListener(listener); 442 } else { 443 mPhotoTouchOverlay.setClickable(false); 444 } 445 } 446 } else { 447 mStaticPhotoContainer.setVisibility(View.GONE); 448 } 449 } 450 451 // Build up the contact entries 452 buildEntries(); 453 454 // Collapse similar data items for select {@link DataKind}s. 455 Collapser.collapseList(mPhoneEntries); 456 Collapser.collapseList(mSmsEntries); 457 Collapser.collapseList(mEmailEntries); 458 Collapser.collapseList(mPostalEntries); 459 Collapser.collapseList(mImEntries); 460 461 mIsUniqueNumber = mPhoneEntries.size() == 1; 462 mIsUniqueEmail = mEmailEntries.size() == 1; 463 464 // Make one aggregated list of all entries for display to the user. 465 setupFlattenedList(); 466 467 if (mAdapter == null) { 468 mAdapter = new ViewAdapter(); 469 mListView.setAdapter(mAdapter); 470 } 471 472 // Restore {@link ListView} state if applicable because the adapter is now populated. 473 if (mListState != null) { 474 mListView.onRestoreInstanceState(mListState); 475 mListState = null; 476 } 477 478 mAdapter.notifyDataSetChanged(); 479 480 mListView.setEmptyView(mEmptyView); 481 482 configureQuickFix(); 483 484 mView.setVisibility(View.VISIBLE); 485 } 486 487 /* 488 * Sets {@link #mQuickFix} to a useful action and configures the visibility of 489 * {@link #mQuickFixButton} 490 */ 491 private void configureQuickFix() { 492 mQuickFix = null; 493 494 for (QuickFix fix : mPotentialQuickFixes) { 495 if (fix.isApplicable()) { 496 mQuickFix = fix; 497 break; 498 } 499 } 500 501 // Configure the button 502 if (mQuickFix == null) { 503 mQuickFixButton.setVisibility(View.GONE); 504 } else { 505 mQuickFixButton.setVisibility(View.VISIBLE); 506 mQuickFixButton.setText(mQuickFix.getTitle()); 507 } 508 } 509 510 /** @return default group id or -1 if no group or several groups are marked as default */ 511 private long getDefaultGroupId(List<GroupMetaData> groups) { 512 long defaultGroupId = -1; 513 for (GroupMetaData group : groups) { 514 if (group.isDefaultGroup()) { 515 // two default groups? return neither 516 if (defaultGroupId != -1) return -1; 517 defaultGroupId = group.getGroupId(); 518 } 519 } 520 return defaultGroupId; 521 } 522 523 /** 524 * Build up the entries to display on the screen. 525 */ 526 private final void buildEntries() { 527 mHasPhone = PhoneCapabilityTester.isPhone(mContext); 528 mHasSms = PhoneCapabilityTester.isSmsIntentRegistered(mContext); 529 mHasSip = PhoneCapabilityTester.isSipPhone(mContext); 530 531 // Clear out the old entries 532 mAllEntries.clear(); 533 534 mRawContactIds.clear(); 535 536 mPrimaryPhoneUri = null; 537 538 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 539 540 // Build up method entries 541 if (mContactData == null) { 542 return; 543 } 544 545 ArrayList<String> groups = new ArrayList<String>(); 546 for (Entity entity: mContactData.getEntities()) { 547 final ContentValues entValues = entity.getEntityValues(); 548 final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE); 549 final String dataSet = entValues.getAsString(RawContacts.DATA_SET); 550 final long rawContactId = entValues.getAsLong(RawContacts._ID); 551 552 if (!mRawContactIds.contains(rawContactId)) { 553 mRawContactIds.add(rawContactId); 554 } 555 556 AccountType type = accountTypes.getAccountType(accountType, dataSet); 557 558 for (NamedContentValues subValue : entity.getSubValues()) { 559 final ContentValues entryValues = subValue.values; 560 entryValues.put(Data.RAW_CONTACT_ID, rawContactId); 561 562 final long dataId = entryValues.getAsLong(Data._ID); 563 final String mimeType = entryValues.getAsString(Data.MIMETYPE); 564 if (mimeType == null) continue; 565 566 if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { 567 Long groupId = entryValues.getAsLong(GroupMembership.GROUP_ROW_ID); 568 if (groupId != null) { 569 handleGroupMembership(groups, mContactData.getGroupMetaData(), groupId); 570 } 571 continue; 572 } 573 574 final DataKind kind = accountTypes.getKindOrFallback( 575 accountType, dataSet, mimeType); 576 if (kind == null) continue; 577 578 final DetailViewEntry entry = DetailViewEntry.fromValues(mContext, mimeType, kind, 579 dataId, entryValues, mContactData.isDirectoryEntry(), 580 mContactData.getDirectoryId()); 581 entry.maxLines = kind.maxLinesForDisplay; 582 583 final boolean hasData = !TextUtils.isEmpty(entry.data); 584 Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY); 585 final boolean isSuperPrimary = superPrimary != null && superPrimary != 0; 586 587 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 588 // Always ignore the name. It is shown in the header if set 589 } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 590 // Build phone entries 591 String phoneNumberE164 = 592 entryValues.getAsString(Phone.NORMALIZED_NUMBER); 593 entry.data = PhoneNumberUtils.formatNumber( 594 entry.data, phoneNumberE164, mDefaultCountryIso); 595 final Intent phoneIntent = mHasPhone ? 596 ContactsUtils.getCallIntent(entry.data) : null; 597 final Intent smsIntent = mHasSms ? new Intent(Intent.ACTION_SENDTO, 598 Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null)) : null; 599 600 // Configure Icons and Intents. 601 if (mHasPhone && mHasSms) { 602 entry.intent = phoneIntent; 603 entry.secondaryIntent = smsIntent; 604 entry.secondaryActionIcon = kind.iconAltRes; 605 entry.secondaryActionDescription = kind.iconAltDescriptionRes; 606 } else if (mHasPhone) { 607 entry.intent = phoneIntent; 608 } else if (mHasSms) { 609 entry.intent = smsIntent; 610 } else { 611 entry.intent = null; 612 } 613 614 // Remember super-primary phone 615 if (isSuperPrimary) mPrimaryPhoneUri = entry.uri; 616 617 entry.isPrimary = isSuperPrimary; 618 619 // If the entry is a primary entry, then render it first in the view. 620 if (entry.isPrimary) { 621 // add to beginning of list so that this phone number shows up first 622 mPhoneEntries.add(0, entry); 623 } else { 624 // add to end of list 625 mPhoneEntries.add(entry); 626 } 627 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 628 // Build email entries 629 entry.intent = new Intent(Intent.ACTION_SENDTO, 630 Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null)); 631 entry.isPrimary = isSuperPrimary; 632 // If entry is a primary entry, then render it first in the view. 633 if (entry.isPrimary) { 634 mEmailEntries.add(0, entry); 635 } else { 636 mEmailEntries.add(entry); 637 } 638 639 // When Email rows have status, create additional Im row 640 final DataStatus status = mContactData.getStatuses().get(entry.id); 641 if (status != null) { 642 final String imMime = Im.CONTENT_ITEM_TYPE; 643 final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet, 644 imMime); 645 final DetailViewEntry imEntry = DetailViewEntry.fromValues(mContext, imMime, 646 imKind, dataId, entryValues, mContactData.isDirectoryEntry(), 647 mContactData.getDirectoryId()); 648 buildImActions(mContext, imEntry, entryValues); 649 imEntry.setPresence(status.getPresence()); 650 imEntry.maxLines = imKind.maxLinesForDisplay; 651 mImEntries.add(imEntry); 652 } 653 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 654 // Build postal entries 655 entry.intent = StructuredPostalUtils.getViewPostalAddressIntent(entry.data); 656 mPostalEntries.add(entry); 657 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 658 // Build IM entries 659 buildImActions(mContext, entry, entryValues); 660 661 // Apply presence when available 662 final DataStatus status = mContactData.getStatuses().get(entry.id); 663 if (status != null) { 664 entry.setPresence(status.getPresence()); 665 } 666 mImEntries.add(entry); 667 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { 668 // Organizations are not shown. The first one is shown in the header 669 // and subsequent ones are not supported anymore 670 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 671 // Build nickname entries 672 final boolean isNameRawContact = 673 (mContactData.getNameRawContactId() == rawContactId); 674 675 final boolean duplicatesTitle = 676 isNameRawContact 677 && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 678 679 if (!duplicatesTitle) { 680 entry.uri = null; 681 mNicknameEntries.add(entry); 682 } 683 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 684 // Build note entries 685 entry.uri = null; 686 mNoteEntries.add(entry); 687 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 688 // Build Website entries 689 entry.uri = null; 690 try { 691 WebAddress webAddress = new WebAddress(entry.data); 692 entry.intent = new Intent(Intent.ACTION_VIEW, 693 Uri.parse(webAddress.toString())); 694 } catch (ParseException e) { 695 Log.e(TAG, "Couldn't parse website: " + entry.data); 696 } 697 mWebsiteEntries.add(entry); 698 } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 699 // Build SipAddress entries 700 entry.uri = null; 701 if (mHasSip) { 702 entry.intent = ContactsUtils.getCallIntent( 703 Uri.fromParts(Constants.SCHEME_SIP, entry.data, null)); 704 } else { 705 entry.intent = null; 706 } 707 mSipEntries.add(entry); 708 // TODO: Now that SipAddress is in its own list of entries 709 // (instead of grouped in mOtherEntries), consider 710 // repositioning it right under the phone number. 711 // (Then, we'd also update FallbackAccountType.java to set 712 // secondary=false for this field, and tweak the weight 713 // of its DataKind.) 714 } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 715 entry.data = DateUtils.formatDate(mContext, entry.data); 716 entry.uri = null; 717 mEventEntries.add(entry); 718 } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) { 719 entry.intent = new Intent(Intent.ACTION_SEARCH); 720 entry.intent.putExtra(SearchManager.QUERY, entry.data); 721 entry.intent.setType(Contacts.CONTENT_TYPE); 722 mRelationEntries.add(entry); 723 } else { 724 // Handle showing custom rows 725 entry.intent = new Intent(Intent.ACTION_VIEW); 726 entry.intent.setDataAndType(entry.uri, entry.mimetype); 727 728 if (kind.actionBody != null) { 729 CharSequence body = kind.actionBody.inflateUsing(mContext, entryValues); 730 entry.data = (body == null) ? null : body.toString(); 731 } 732 733 if (!TextUtils.isEmpty(entry.data)) { 734 // If the account type exists in the hash map, add it as another entry for 735 // that account type 736 if (mOtherEntriesMap.containsKey(type)) { 737 List<DetailViewEntry> listEntries = mOtherEntriesMap.get(type); 738 listEntries.add(entry); 739 } else { 740 // Otherwise create a new list with the entry and add it to the hash map 741 List<DetailViewEntry> listEntries = new ArrayList<DetailViewEntry>(); 742 listEntries.add(entry); 743 mOtherEntriesMap.put(type, listEntries); 744 } 745 } 746 } 747 } 748 } 749 750 if (!groups.isEmpty()) { 751 DetailViewEntry entry = new DetailViewEntry(); 752 Collections.sort(groups); 753 StringBuilder sb = new StringBuilder(); 754 int size = groups.size(); 755 for (int i = 0; i < size; i++) { 756 if (i != 0) { 757 sb.append(", "); 758 } 759 sb.append(groups.get(i)); 760 } 761 entry.mimetype = GroupMembership.MIMETYPE; 762 entry.kind = mContext.getString(R.string.groupsLabel); 763 entry.data = sb.toString(); 764 mGroupEntries.add(entry); 765 } 766 } 767 768 /** 769 * Collapse all contact detail entries into one aggregated list with a {@link HeaderViewEntry} 770 * at the top. 771 */ 772 private void setupFlattenedList() { 773 // All contacts should have a header view (even if there is no data for the contact). 774 mAllEntries.add(new HeaderViewEntry()); 775 776 addPhoneticName(); 777 778 flattenList(mPhoneEntries); 779 flattenList(mSmsEntries); 780 flattenList(mEmailEntries); 781 flattenList(mImEntries); 782 flattenList(mNicknameEntries); 783 flattenList(mWebsiteEntries); 784 785 addNetworks(); 786 787 flattenList(mSipEntries); 788 flattenList(mPostalEntries); 789 flattenList(mEventEntries); 790 flattenList(mGroupEntries); 791 flattenList(mRelationEntries); 792 flattenList(mNoteEntries); 793 } 794 795 /** 796 * Add phonetic name (if applicable) to the aggregated list of contact details. This has to be 797 * done manually because phonetic name doesn't have a mimetype or action intent. 798 */ 799 private void addPhoneticName() { 800 String phoneticName = ContactDetailDisplayUtils.getPhoneticName(mContext, mContactData); 801 if (TextUtils.isEmpty(phoneticName)) { 802 return; 803 } 804 805 // Add a title 806 String phoneticNameKindTitle = mContext.getString(R.string.name_phonetic); 807 mAllEntries.add(new KindTitleViewEntry(phoneticNameKindTitle.toUpperCase())); 808 809 // Add the phonetic name 810 final DetailViewEntry entry = new DetailViewEntry(); 811 entry.kind = phoneticNameKindTitle; 812 entry.data = phoneticName; 813 mAllEntries.add(entry); 814 } 815 816 /** 817 * Add attribution and other third-party entries (if applicable) under the "networks" section 818 * of the aggregated list of contact details. This has to be done manually because the 819 * attribution does not have a mimetype and the third-party entries don't have actually belong 820 * to the same {@link DataKind}. 821 */ 822 private void addNetworks() { 823 String attribution = ContactDetailDisplayUtils.getAttribution(mContext, mContactData); 824 boolean hasAttribution = !TextUtils.isEmpty(attribution); 825 int networksCount = mOtherEntriesMap.keySet().size(); 826 827 // Note: invitableCount will always be 0 for me profile. (ContactLoader won't set 828 // invitable types for me profile.) 829 int invitableCount = mContactData.getInvitableAccountTypes().size(); 830 if (!hasAttribution && networksCount == 0 && invitableCount == 0) { 831 return; 832 } 833 834 // Add a title 835 String networkKindTitle = mContext.getString(R.string.connections); 836 mAllEntries.add(new KindTitleViewEntry(networkKindTitle.toUpperCase())); 837 838 // Add the attribution if applicable 839 if (hasAttribution) { 840 final DetailViewEntry entry = new DetailViewEntry(); 841 entry.kind = networkKindTitle; 842 entry.data = attribution; 843 mAllEntries.add(entry); 844 845 // Add a divider below the attribution if there are network details that will follow 846 if (networksCount > 0) { 847 mAllEntries.add(new SeparatorViewEntry()); 848 } 849 } 850 851 // Add the other entries from third parties 852 for (AccountType accountType : mOtherEntriesMap.keySet()) { 853 854 // Add a title for each third party app 855 mAllEntries.add(new NetworkTitleViewEntry(mContext, accountType)); 856 857 for (DetailViewEntry detailEntry : mOtherEntriesMap.get(accountType)) { 858 // Add indented separator 859 SeparatorViewEntry separatorEntry = new SeparatorViewEntry(); 860 separatorEntry.setIsInSubSection(true); 861 mAllEntries.add(separatorEntry); 862 863 // Add indented detail 864 detailEntry.setIsInSubSection(true); 865 mAllEntries.add(detailEntry); 866 } 867 } 868 869 mOtherEntriesMap.clear(); 870 871 // Add the "More networks" button, which opens the invitable account type list popup. 872 if (invitableCount > 0) { 873 addMoreNetworks(); 874 } 875 } 876 877 /** 878 * Add the "More networks" entry. When clicked, show a popup containing a list of invitable 879 * account types. 880 */ 881 private void addMoreNetworks() { 882 // First, prepare for the popup. 883 884 // Adapter for the list popup. 885 final InvitableAccountTypesAdapter popupAdapter = new InvitableAccountTypesAdapter(mContext, 886 mContactData); 887 888 // Listener called when a popup item is clicked. 889 final AdapterView.OnItemClickListener popupItemListener 890 = new AdapterView.OnItemClickListener() { 891 @Override 892 public void onItemClick(AdapterView<?> parent, View view, int position, 893 long id) { 894 if (mListener != null && mContactData != null) { 895 mListener.onItemClicked(ContactsUtils.getInvitableIntent( 896 popupAdapter.getItem(position) /* account type */, 897 mContactData.getLookupUri())); 898 } 899 } 900 }; 901 902 // Then create the click listener for the "More network" entry. Open the popup. 903 View.OnClickListener onClickListener = new OnClickListener() { 904 @Override 905 public void onClick(View v) { 906 showListPopup(v, popupAdapter, popupItemListener); 907 } 908 }; 909 910 // Finally create the entry. 911 mAllEntries.add(new AddConnectionViewEntry(mContext, onClickListener)); 912 } 913 914 /** 915 * Iterate through {@link DetailViewEntry} in the given list and add it to a list of all 916 * entries. Add a {@link KindTitleViewEntry} at the start if the length of the list is not 0. 917 * Add {@link SeparatorViewEntry}s as dividers as appropriate. Clear the original list. 918 */ 919 private void flattenList(ArrayList<DetailViewEntry> entries) { 920 int count = entries.size(); 921 922 // Add a title for this kind by extracting the kind from the first entry 923 if (count > 0) { 924 String kind = entries.get(0).kind; 925 mAllEntries.add(new KindTitleViewEntry(kind.toUpperCase())); 926 } 927 928 // Add all the data entries for this kind 929 for (int i = 0; i < count; i++) { 930 // For all entries except the first one, add a divider above the entry 931 if (i != 0) { 932 mAllEntries.add(new SeparatorViewEntry()); 933 } 934 mAllEntries.add(entries.get(i)); 935 } 936 937 // Clear old list because it's not needed anymore. 938 entries.clear(); 939 } 940 941 /** 942 * Maps group ID to the corresponding group name, collapses all synonymous groups. 943 * Ignores default groups (e.g. My Contacts) and favorites groups. 944 */ 945 private void handleGroupMembership( 946 ArrayList<String> groups, List<GroupMetaData> groupMetaData, long groupId) { 947 if (groupMetaData == null) { 948 return; 949 } 950 951 for (GroupMetaData group : groupMetaData) { 952 if (group.getGroupId() == groupId) { 953 if (!group.isDefaultGroup() && !group.isFavorites()) { 954 String title = group.getTitle(); 955 if (!TextUtils.isEmpty(title) && !groups.contains(title)) { 956 groups.add(title); 957 } 958 } 959 break; 960 } 961 } 962 } 963 964 private static String buildDataString(DataKind kind, ContentValues values, 965 Context context) { 966 if (kind.actionBody == null) { 967 return null; 968 } 969 CharSequence actionBody = kind.actionBody.inflateUsing(context, values); 970 return actionBody == null ? null : actionBody.toString(); 971 } 972 973 /** 974 * Writes the Instant Messaging action into the given entry value. 975 */ 976 @VisibleForTesting 977 public static void buildImActions(Context context, DetailViewEntry entry, 978 ContentValues values) { 979 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(values.getAsString(Data.MIMETYPE)); 980 981 if (!isEmail && !isProtocolValid(values)) { 982 return; 983 } 984 985 final String data = values.getAsString(isEmail ? Email.DATA : Im.DATA); 986 if (TextUtils.isEmpty(data)) { 987 return; 988 } 989 990 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : values.getAsInteger(Im.PROTOCOL); 991 992 if (protocol == Im.PROTOCOL_GOOGLE_TALK) { 993 final Integer chatCapabilityObj = values.getAsInteger(Im.CHAT_CAPABILITY); 994 final int chatCapability = chatCapabilityObj == null ? 0 : chatCapabilityObj; 995 entry.chatCapability = chatCapability; 996 entry.typeString = Im.getProtocolLabel(context.getResources(), Im.PROTOCOL_GOOGLE_TALK, 997 null).toString(); 998 if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { 999 entry.intent = 1000 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); 1001 entry.secondaryIntent = 1002 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); 1003 } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { 1004 // Allow Talking and Texting 1005 entry.intent = 1006 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); 1007 entry.secondaryIntent = 1008 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); 1009 } else { 1010 entry.intent = 1011 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); 1012 } 1013 } else { 1014 // Build an IM Intent 1015 String host = values.getAsString(Im.CUSTOM_PROTOCOL); 1016 1017 if (protocol != Im.PROTOCOL_CUSTOM) { 1018 // Try bringing in a well-known host for specific protocols 1019 host = ContactsUtils.lookupProviderNameFromId(protocol); 1020 } 1021 1022 if (!TextUtils.isEmpty(host)) { 1023 final String authority = host.toLowerCase(); 1024 final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority( 1025 authority).appendPath(data).build(); 1026 entry.intent = new Intent(Intent.ACTION_SENDTO, imUri); 1027 } 1028 } 1029 } 1030 1031 private static boolean isProtocolValid(ContentValues values) { 1032 String protocolString = values.getAsString(Im.PROTOCOL); 1033 if (protocolString == null) { 1034 return false; 1035 } 1036 try { 1037 Integer.valueOf(protocolString); 1038 } catch (NumberFormatException e) { 1039 return false; 1040 } 1041 return true; 1042 } 1043 1044 /** 1045 * Show a list popup. Used for "popup-able" entry, such as "More networks". 1046 */ 1047 private void showListPopup(View anchorView, ListAdapter adapter, 1048 final AdapterView.OnItemClickListener onItemClickListener) { 1049 dismissPopupIfShown(); 1050 mPopup = new ListPopupWindow(mContext, null); 1051 mPopup.setAnchorView(anchorView); 1052 mPopup.setWidth(anchorView.getWidth()); 1053 mPopup.setAdapter(adapter); 1054 mPopup.setModal(true); 1055 1056 // We need to wrap the passed onItemClickListener here, so that we can dismiss() the 1057 // popup afterwards. Otherwise we could directly use the passed listener. 1058 mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1059 @Override 1060 public void onItemClick(AdapterView<?> parent, View view, int position, 1061 long id) { 1062 onItemClickListener.onItemClick(parent, view, position, id); 1063 dismissPopupIfShown(); 1064 } 1065 }); 1066 mPopup.show(); 1067 } 1068 1069 private void dismissPopupIfShown() { 1070 if (mPopup != null && mPopup.isShowing()) { 1071 mPopup.dismiss(); 1072 } 1073 mPopup = null; 1074 } 1075 1076 /** 1077 * Base class for an item in the {@link ViewAdapter} list of data, which is 1078 * supplied to the {@link ListView}. 1079 */ 1080 static class ViewEntry { 1081 private final int viewTypeForAdapter; 1082 protected long id = -1; 1083 /** Whether or not the entry can be focused on or not. */ 1084 protected boolean isEnabled = false; 1085 1086 ViewEntry(int viewType) { 1087 viewTypeForAdapter = viewType; 1088 } 1089 1090 int getViewType() { 1091 return viewTypeForAdapter; 1092 } 1093 1094 long getId() { 1095 return id; 1096 } 1097 1098 boolean isEnabled(){ 1099 return isEnabled; 1100 } 1101 1102 /** 1103 * Called when the entry is clicked. Only {@link #isEnabled} entries can get clicked. 1104 * 1105 * @param clickedView {@link View} that was clicked (Used, for example, as the anchor view 1106 * for a popup.) 1107 * @param fragmentListener {@link Listener} set to {@link ContactDetailFragment} 1108 */ 1109 public void click(View clickedView, Listener fragmentListener) { 1110 } 1111 } 1112 1113 /** 1114 * Header item in the {@link ViewAdapter} list of data. 1115 */ 1116 private static class HeaderViewEntry extends ViewEntry { 1117 1118 HeaderViewEntry() { 1119 super(ViewAdapter.VIEW_TYPE_HEADER_ENTRY); 1120 } 1121 1122 } 1123 1124 /** 1125 * Separator between items of the same {@link DataKind} in the 1126 * {@link ViewAdapter} list of data. 1127 */ 1128 private static class SeparatorViewEntry extends ViewEntry { 1129 1130 /** 1131 * Whether or not the entry is in a subsection (if true then the contents will be indented 1132 * to the right) 1133 */ 1134 private boolean mIsInSubSection = false; 1135 1136 SeparatorViewEntry() { 1137 super(ViewAdapter.VIEW_TYPE_SEPARATOR_ENTRY); 1138 } 1139 1140 public void setIsInSubSection(boolean isInSubSection) { 1141 mIsInSubSection = isInSubSection; 1142 } 1143 1144 public boolean isInSubSection() { 1145 return mIsInSubSection; 1146 } 1147 } 1148 1149 /** 1150 * Title entry for items of the same {@link DataKind} in the 1151 * {@link ViewAdapter} list of data. 1152 */ 1153 private static class KindTitleViewEntry extends ViewEntry { 1154 1155 private final String mTitle; 1156 1157 KindTitleViewEntry(String titleText) { 1158 super(ViewAdapter.VIEW_TYPE_KIND_TITLE_ENTRY); 1159 mTitle = titleText; 1160 } 1161 1162 public String getTitle() { 1163 return mTitle; 1164 } 1165 } 1166 1167 /** 1168 * A title for a section of contact details from a single 3rd party network. 1169 */ 1170 private static class NetworkTitleViewEntry extends ViewEntry { 1171 private final Drawable mIcon; 1172 private final CharSequence mLabel; 1173 1174 public NetworkTitleViewEntry(Context context, AccountType type) { 1175 super(ViewAdapter.VIEW_TYPE_NETWORK_TITLE_ENTRY); 1176 this.mIcon = type.getDisplayIcon(context); 1177 this.mLabel = type.getDisplayLabel(context); 1178 this.isEnabled = false; 1179 } 1180 1181 public Drawable getIcon() { 1182 return mIcon; 1183 } 1184 1185 public CharSequence getLabel() { 1186 return mLabel; 1187 } 1188 } 1189 1190 /** 1191 * This is used for the "Add Connections" entry. 1192 */ 1193 private static class AddConnectionViewEntry extends ViewEntry { 1194 private final Drawable mIcon; 1195 private final CharSequence mLabel; 1196 private final View.OnClickListener mOnClickListener; 1197 1198 private AddConnectionViewEntry(Context context, View.OnClickListener onClickListener) { 1199 super(ViewAdapter.VIEW_TYPE_ADD_CONNECTION_ENTRY); 1200 this.mIcon = context.getResources().getDrawable( 1201 R.drawable.ic_menu_add_field_holo_light); 1202 this.mLabel = context.getString(R.string.add_connection_button); 1203 this.mOnClickListener = onClickListener; 1204 this.isEnabled = true; 1205 } 1206 1207 @Override 1208 public void click(View clickedView, Listener fragmentListener) { 1209 if (mOnClickListener == null) return; 1210 mOnClickListener.onClick(clickedView); 1211 } 1212 1213 public Drawable getIcon() { 1214 return mIcon; 1215 } 1216 1217 public CharSequence getLabel() { 1218 return mLabel; 1219 } 1220 } 1221 1222 /** 1223 * An item with a single detail for a contact in the {@link ViewAdapter} 1224 * list of data. 1225 */ 1226 static class DetailViewEntry extends ViewEntry implements Collapsible<DetailViewEntry> { 1227 // TODO: Make getters/setters for these fields 1228 public int type = -1; 1229 public String kind; 1230 public String typeString; 1231 public String data; 1232 public Uri uri; 1233 public int maxLines = 1; 1234 public String mimetype; 1235 1236 public Context context = null; 1237 public boolean isPrimary = false; 1238 public int secondaryActionIcon = -1; 1239 public int secondaryActionDescription = -1; 1240 public Intent intent; 1241 public Intent secondaryIntent = null; 1242 public ArrayList<Long> ids = new ArrayList<Long>(); 1243 public int collapseCount = 0; 1244 1245 public int presence = -1; 1246 public int chatCapability = 0; 1247 1248 private boolean mIsInSubSection = false; 1249 1250 DetailViewEntry() { 1251 super(ViewAdapter.VIEW_TYPE_DETAIL_ENTRY); 1252 isEnabled = true; 1253 } 1254 1255 /** 1256 * Build new {@link DetailViewEntry} and populate from the given values. 1257 */ 1258 public static DetailViewEntry fromValues(Context context, String mimeType, DataKind kind, 1259 long dataId, ContentValues values, boolean isDirectoryEntry, long directoryId) { 1260 final DetailViewEntry entry = new DetailViewEntry(); 1261 entry.id = dataId; 1262 entry.context = context; 1263 entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id); 1264 if (isDirectoryEntry) { 1265 entry.uri = entry.uri.buildUpon().appendQueryParameter( 1266 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build(); 1267 } 1268 entry.mimetype = mimeType; 1269 entry.kind = (kind.titleRes == -1 || kind.titleRes == 0) ? "" 1270 : context.getString(kind.titleRes); 1271 entry.data = buildDataString(kind, values, context); 1272 1273 if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) { 1274 entry.type = values.getAsInteger(kind.typeColumn); 1275 1276 // get type string 1277 entry.typeString = ""; 1278 for (EditType type : kind.typeList) { 1279 if (type.rawValue == entry.type) { 1280 if (type.customColumn == null) { 1281 // Non-custom type. Get its description from the resource 1282 entry.typeString = context.getString(type.labelRes); 1283 } else { 1284 // Custom type. Read it from the database 1285 entry.typeString = values.getAsString(type.customColumn); 1286 } 1287 break; 1288 } 1289 } 1290 } else { 1291 entry.typeString = ""; 1292 } 1293 1294 return entry; 1295 } 1296 1297 public void setPresence(int presence) { 1298 this.presence = presence; 1299 } 1300 1301 public void setIsInSubSection(boolean isInSubSection) { 1302 mIsInSubSection = isInSubSection; 1303 } 1304 1305 public boolean isInSubSection() { 1306 return mIsInSubSection; 1307 } 1308 1309 @Override 1310 public boolean collapseWith(DetailViewEntry entry) { 1311 // assert equal collapse keys 1312 if (!shouldCollapseWith(entry)) { 1313 return false; 1314 } 1315 1316 // Choose the label associated with the highest type precedence. 1317 if (TypePrecedence.getTypePrecedence(mimetype, type) 1318 > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) { 1319 type = entry.type; 1320 kind = entry.kind; 1321 typeString = entry.typeString; 1322 } 1323 1324 // Choose the max of the maxLines and maxLabelLines values. 1325 maxLines = Math.max(maxLines, entry.maxLines); 1326 1327 // Choose the presence with the highest precedence. 1328 if (StatusUpdates.getPresencePrecedence(presence) 1329 < StatusUpdates.getPresencePrecedence(entry.presence)) { 1330 presence = entry.presence; 1331 } 1332 1333 // If any of the collapsed entries are primary make the whole thing primary. 1334 isPrimary = entry.isPrimary ? true : isPrimary; 1335 1336 // uri, and contactdId, shouldn't make a difference. Just keep the original. 1337 1338 // Keep track of all the ids that have been collapsed with this one. 1339 ids.add(entry.getId()); 1340 collapseCount++; 1341 return true; 1342 } 1343 1344 @Override 1345 public boolean shouldCollapseWith(DetailViewEntry entry) { 1346 if (entry == null) { 1347 return false; 1348 } 1349 1350 if (!ContactsUtils.shouldCollapse(mimetype, data, entry.mimetype, entry.data)) { 1351 return false; 1352 } 1353 1354 if (!TextUtils.equals(mimetype, entry.mimetype) 1355 || !ContactsUtils.areIntentActionEqual(intent, entry.intent) 1356 || !ContactsUtils.areIntentActionEqual( 1357 secondaryIntent, entry.secondaryIntent)) { 1358 return false; 1359 } 1360 1361 return true; 1362 } 1363 1364 @Override 1365 public void click(View clickedView, Listener fragmentListener) { 1366 if (fragmentListener == null || intent == null) return; 1367 fragmentListener.onItemClicked(intent); 1368 } 1369 } 1370 1371 /** 1372 * Cache of the children views for a view that displays a header view entry. 1373 */ 1374 private static class HeaderViewCache { 1375 public final TextView displayNameView; 1376 public final TextView companyView; 1377 public final ImageView photoView; 1378 public final View photoOverlayView; 1379 public final ImageView starredView; 1380 public final int layoutResourceId; 1381 1382 public HeaderViewCache(View view, int layoutResourceInflated) { 1383 displayNameView = (TextView) view.findViewById(R.id.name); 1384 companyView = (TextView) view.findViewById(R.id.company); 1385 photoView = (ImageView) view.findViewById(R.id.photo); 1386 photoOverlayView = view.findViewById(R.id.photo_touch_intercept_overlay); 1387 starredView = (ImageView) view.findViewById(R.id.star); 1388 layoutResourceId = layoutResourceInflated; 1389 } 1390 1391 public void enablePhotoOverlay(OnClickListener listener) { 1392 if (photoOverlayView != null) { 1393 photoOverlayView.setOnClickListener(listener); 1394 photoOverlayView.setVisibility(View.VISIBLE); 1395 } 1396 } 1397 } 1398 1399 private static class KindTitleViewCache { 1400 public final TextView titleView; 1401 1402 public KindTitleViewCache(View view) { 1403 titleView = (TextView)view.findViewById(R.id.title); 1404 } 1405 } 1406 1407 /** 1408 * Cache of the children views for a view that displays a {@link NetworkTitleViewEntry} 1409 */ 1410 private static class NetworkTitleViewCache { 1411 public final TextView name; 1412 public final ImageView icon; 1413 1414 public NetworkTitleViewCache(View view) { 1415 name = (TextView) view.findViewById(R.id.network_title); 1416 icon = (ImageView) view.findViewById(R.id.network_icon); 1417 } 1418 } 1419 1420 /** 1421 * Cache of the children views for a view that displays a {@link AddConnectionViewEntry} 1422 */ 1423 private static class AddConnectionViewCache { 1424 public final TextView name; 1425 public final ImageView icon; 1426 public final View primaryActionView; 1427 1428 public AddConnectionViewCache(View view) { 1429 name = (TextView) view.findViewById(R.id.add_connection_label); 1430 icon = (ImageView) view.findViewById(R.id.add_connection_icon); 1431 primaryActionView = view.findViewById(R.id.primary_action_view); 1432 } 1433 } 1434 1435 /** 1436 * Cache of the children views of a contact detail entry represented by a 1437 * {@link DetailViewEntry} 1438 */ 1439 private static class DetailViewCache { 1440 public final TextView type; 1441 public final TextView data; 1442 public final ImageView presenceIcon; 1443 public final ImageView secondaryActionButton; 1444 public final View actionsViewContainer; 1445 public final View primaryActionView; 1446 public final View secondaryActionViewContainer; 1447 public final View secondaryActionDivider; 1448 public final View primaryIndicator; 1449 1450 public DetailViewCache(View view, 1451 OnClickListener primaryActionClickListener, 1452 OnClickListener secondaryActionClickListener) { 1453 type = (TextView) view.findViewById(R.id.type); 1454 data = (TextView) view.findViewById(R.id.data); 1455 primaryIndicator = view.findViewById(R.id.primary_indicator); 1456 presenceIcon = (ImageView) view.findViewById(R.id.presence_icon); 1457 1458 actionsViewContainer = view.findViewById(R.id.actions_view_container); 1459 actionsViewContainer.setOnClickListener(primaryActionClickListener); 1460 primaryActionView = view.findViewById(R.id.primary_action_view); 1461 1462 secondaryActionViewContainer = view.findViewById( 1463 R.id.secondary_action_view_container); 1464 secondaryActionViewContainer.setOnClickListener( 1465 secondaryActionClickListener); 1466 secondaryActionButton = (ImageView) view.findViewById( 1467 R.id.secondary_action_button); 1468 1469 secondaryActionDivider = view.findViewById(R.id.vertical_divider); 1470 } 1471 } 1472 1473 private final class ViewAdapter extends BaseAdapter { 1474 1475 public static final int VIEW_TYPE_DETAIL_ENTRY = 0; 1476 public static final int VIEW_TYPE_HEADER_ENTRY = 1; 1477 public static final int VIEW_TYPE_KIND_TITLE_ENTRY = 2; 1478 public static final int VIEW_TYPE_NETWORK_TITLE_ENTRY = 3; 1479 public static final int VIEW_TYPE_ADD_CONNECTION_ENTRY = 4; 1480 public static final int VIEW_TYPE_SEPARATOR_ENTRY = 5; 1481 private static final int VIEW_TYPE_COUNT = 6; 1482 1483 @Override 1484 public View getView(int position, View convertView, ViewGroup parent) { 1485 switch (getItemViewType(position)) { 1486 case VIEW_TYPE_HEADER_ENTRY: 1487 return getHeaderEntryView(convertView, parent); 1488 case VIEW_TYPE_SEPARATOR_ENTRY: 1489 return getSeparatorEntryView(position, convertView, parent); 1490 case VIEW_TYPE_KIND_TITLE_ENTRY: 1491 return getKindTitleEntryView(position, convertView, parent); 1492 case VIEW_TYPE_DETAIL_ENTRY: 1493 return getDetailEntryView(position, convertView, parent); 1494 case VIEW_TYPE_NETWORK_TITLE_ENTRY: 1495 return getNetworkTitleEntryView(position, convertView, parent); 1496 case VIEW_TYPE_ADD_CONNECTION_ENTRY: 1497 return getAddConnectionEntryView(position, convertView, parent); 1498 default: 1499 throw new IllegalStateException("Invalid view type ID " + 1500 getItemViewType(position)); 1501 } 1502 } 1503 1504 private View getHeaderEntryView(View convertView, ViewGroup parent) { 1505 final int desiredLayoutResourceId = mContactHasSocialUpdates ? 1506 R.layout.detail_header_contact_with_updates : 1507 R.layout.detail_header_contact_without_updates; 1508 View result = null; 1509 HeaderViewCache viewCache = null; 1510 1511 // Only use convertView if it has the same layout resource ID as the one desired 1512 // (the two can be different on wide 2-pane screens where the detail fragment is reused 1513 // for many different contacts that do and do not have social updates). 1514 if (convertView != null) { 1515 viewCache = (HeaderViewCache) convertView.getTag(); 1516 if (viewCache.layoutResourceId == desiredLayoutResourceId) { 1517 result = convertView; 1518 } 1519 } 1520 1521 // Otherwise inflate a new header view and create a new view cache. 1522 if (result == null) { 1523 result = mInflater.inflate(desiredLayoutResourceId, parent, false); 1524 viewCache = new HeaderViewCache(result, desiredLayoutResourceId); 1525 result.setTag(viewCache); 1526 } 1527 1528 ContactDetailDisplayUtils.setDisplayName(mContext, mContactData, 1529 viewCache.displayNameView); 1530 ContactDetailDisplayUtils.setCompanyName(mContext, mContactData, viewCache.companyView); 1531 1532 // Set the photo if it should be displayed 1533 if (viewCache.photoView != null) { 1534 final boolean expandOnClick = mContactData.getPhotoUri() != null; 1535 final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick( 1536 mContext, mContactData, viewCache.photoView, expandOnClick); 1537 1538 if (expandOnClick || mContactData.isWritableContact(mContext)) { 1539 viewCache.enablePhotoOverlay(listener); 1540 } 1541 } 1542 1543 // Set the starred state if it should be displayed 1544 final ImageView favoritesStar = viewCache.starredView; 1545 if (favoritesStar != null) { 1546 ContactDetailDisplayUtils.configureStarredImageView(favoritesStar, 1547 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1548 mContactData.getStarred()); 1549 final Uri lookupUri = mContactData.getLookupUri(); 1550 favoritesStar.setOnClickListener(new OnClickListener() { 1551 @Override 1552 public void onClick(View v) { 1553 // Toggle "starred" state 1554 // Make sure there is a contact 1555 if (lookupUri != null) { 1556 // Read the current starred value from the UI instead of using the last 1557 // loaded state. This allows rapid tapping without writing the same 1558 // value several times 1559 final Object tag = favoritesStar.getTag(); 1560 final boolean isStarred = tag == null 1561 ? false : (Boolean) favoritesStar.getTag(); 1562 1563 // To improve responsiveness, swap out the picture (and tag) in the UI 1564 // already 1565 ContactDetailDisplayUtils.configureStarredImageView(favoritesStar, 1566 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1567 !isStarred); 1568 1569 // Now perform the real save 1570 Intent intent = ContactSaveService.createSetStarredIntent( 1571 getContext(), lookupUri, !isStarred); 1572 getContext().startService(intent); 1573 } 1574 } 1575 }); 1576 } 1577 1578 return result; 1579 } 1580 1581 private View getSeparatorEntryView(int position, View convertView, ViewGroup parent) { 1582 final SeparatorViewEntry entry = (SeparatorViewEntry) getItem(position); 1583 final View result = (convertView != null) ? convertView : 1584 mInflater.inflate(R.layout.contact_detail_separator_entry_view, parent, false); 1585 1586 result.setPadding(entry.isInSubSection() ? mViewEntryDimensions.getWidePaddingLeft() : 1587 mViewEntryDimensions.getPaddingLeft(), 0, 1588 mViewEntryDimensions.getPaddingRight(), 0); 1589 1590 return result; 1591 } 1592 1593 private View getKindTitleEntryView(int position, View convertView, ViewGroup parent) { 1594 final KindTitleViewEntry entry = (KindTitleViewEntry) getItem(position); 1595 final View result; 1596 final KindTitleViewCache viewCache; 1597 1598 if (convertView != null) { 1599 result = convertView; 1600 viewCache = (KindTitleViewCache)result.getTag(); 1601 } else { 1602 result = mInflater.inflate(R.layout.list_separator, parent, false); 1603 viewCache = new KindTitleViewCache(result); 1604 result.setTag(viewCache); 1605 } 1606 1607 viewCache.titleView.setText(entry.getTitle()); 1608 1609 return result; 1610 } 1611 1612 private View getNetworkTitleEntryView(int position, View convertView, ViewGroup parent) { 1613 final NetworkTitleViewEntry entry = (NetworkTitleViewEntry) getItem(position); 1614 final View result; 1615 final NetworkTitleViewCache viewCache; 1616 1617 if (convertView != null) { 1618 result = convertView; 1619 viewCache = (NetworkTitleViewCache) result.getTag(); 1620 } else { 1621 result = mInflater.inflate(R.layout.contact_detail_network_title_entry_view, 1622 parent, false); 1623 viewCache = new NetworkTitleViewCache(result); 1624 result.setTag(viewCache); 1625 } 1626 1627 viewCache.name.setText(entry.getLabel()); 1628 viewCache.icon.setImageDrawable(entry.getIcon()); 1629 1630 return result; 1631 } 1632 1633 private View getAddConnectionEntryView(int position, View convertView, ViewGroup parent) { 1634 final AddConnectionViewEntry entry = (AddConnectionViewEntry) getItem(position); 1635 final View result; 1636 final AddConnectionViewCache viewCache; 1637 1638 if (convertView != null) { 1639 result = convertView; 1640 viewCache = (AddConnectionViewCache) result.getTag(); 1641 } else { 1642 result = mInflater.inflate(R.layout.contact_detail_add_connection_entry_view, 1643 parent, false); 1644 viewCache = new AddConnectionViewCache(result); 1645 result.setTag(viewCache); 1646 } 1647 viewCache.name.setText(entry.getLabel()); 1648 viewCache.icon.setImageDrawable(entry.getIcon()); 1649 viewCache.primaryActionView.setOnClickListener(entry.mOnClickListener); 1650 1651 return result; 1652 } 1653 1654 private View getDetailEntryView(int position, View convertView, ViewGroup parent) { 1655 final DetailViewEntry entry = (DetailViewEntry) getItem(position); 1656 final View v; 1657 final DetailViewCache viewCache; 1658 1659 // Check to see if we can reuse convertView 1660 if (convertView != null) { 1661 v = convertView; 1662 viewCache = (DetailViewCache) v.getTag(); 1663 } else { 1664 // Create a new view if needed 1665 v = mInflater.inflate(R.layout.contact_detail_list_item, parent, false); 1666 1667 // Cache the children 1668 viewCache = new DetailViewCache(v, 1669 mPrimaryActionClickListener, mSecondaryActionClickListener); 1670 v.setTag(viewCache); 1671 } 1672 1673 bindDetailView(position, v, entry); 1674 return v; 1675 } 1676 1677 private void bindDetailView(int position, View view, DetailViewEntry entry) { 1678 final Resources resources = mContext.getResources(); 1679 DetailViewCache views = (DetailViewCache) view.getTag(); 1680 1681 if (!TextUtils.isEmpty(entry.typeString)) { 1682 views.type.setText(entry.typeString.toUpperCase()); 1683 views.type.setVisibility(View.VISIBLE); 1684 } else { 1685 views.type.setVisibility(View.GONE); 1686 } 1687 1688 views.data.setText(entry.data); 1689 setMaxLines(views.data, entry.maxLines); 1690 1691 // Set the default contact method 1692 views.primaryIndicator.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE); 1693 1694 // Set the presence icon 1695 final Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon( 1696 mContext, entry.presence); 1697 final ImageView presenceIconView = views.presenceIcon; 1698 if (presenceIcon != null) { 1699 presenceIconView.setImageDrawable(presenceIcon); 1700 presenceIconView.setVisibility(View.VISIBLE); 1701 } else { 1702 presenceIconView.setVisibility(View.GONE); 1703 } 1704 1705 final ActionsViewContainer actionsButtonContainer = 1706 (ActionsViewContainer) views.actionsViewContainer; 1707 actionsButtonContainer.setTag(entry); 1708 actionsButtonContainer.setPosition(position); 1709 registerForContextMenu(actionsButtonContainer); 1710 1711 // Set the secondary action button 1712 final ImageView secondaryActionView = views.secondaryActionButton; 1713 Drawable secondaryActionIcon = null; 1714 String secondaryActionDescription = null; 1715 if (entry.secondaryActionIcon != -1) { 1716 secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon); 1717 secondaryActionDescription = resources.getString(entry.secondaryActionDescription); 1718 } else if ((entry.chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { 1719 secondaryActionIcon = 1720 resources.getDrawable(R.drawable.sym_action_videochat_holo_light); 1721 secondaryActionDescription = resources.getString(R.string.video_chat); 1722 } else if ((entry.chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { 1723 secondaryActionIcon = 1724 resources.getDrawable(R.drawable.sym_action_audiochat_holo_light); 1725 secondaryActionDescription = resources.getString(R.string.audio_chat); 1726 } 1727 1728 final View secondaryActionViewContainer = views.secondaryActionViewContainer; 1729 if (entry.secondaryIntent != null && secondaryActionIcon != null) { 1730 secondaryActionView.setImageDrawable(secondaryActionIcon); 1731 secondaryActionView.setContentDescription(secondaryActionDescription); 1732 secondaryActionViewContainer.setTag(entry); 1733 secondaryActionViewContainer.setVisibility(View.VISIBLE); 1734 views.secondaryActionDivider.setVisibility(View.VISIBLE); 1735 } else { 1736 secondaryActionViewContainer.setVisibility(View.GONE); 1737 views.secondaryActionDivider.setVisibility(View.GONE); 1738 } 1739 1740 // Right and left padding should not have "pressed" effect. 1741 view.setPadding( 1742 entry.isInSubSection() 1743 ? mViewEntryDimensions.getWidePaddingLeft() 1744 : mViewEntryDimensions.getPaddingLeft(), 1745 0, mViewEntryDimensions.getPaddingRight(), 0); 1746 // Top and bottom padding should have "pressed" effect. 1747 final View primaryActionView = views.primaryActionView; 1748 primaryActionView.setPadding( 1749 primaryActionView.getPaddingLeft(), 1750 mViewEntryDimensions.getPaddingTop(), 1751 primaryActionView.getPaddingRight(), 1752 mViewEntryDimensions.getPaddingBottom()); 1753 secondaryActionViewContainer.setPadding( 1754 secondaryActionViewContainer.getPaddingLeft(), 1755 mViewEntryDimensions.getPaddingTop(), 1756 secondaryActionViewContainer.getPaddingRight(), 1757 mViewEntryDimensions.getPaddingBottom()); 1758 } 1759 1760 private void setMaxLines(TextView textView, int maxLines) { 1761 if (maxLines == 1) { 1762 textView.setSingleLine(true); 1763 textView.setEllipsize(TextUtils.TruncateAt.END); 1764 } else { 1765 textView.setSingleLine(false); 1766 textView.setMaxLines(maxLines); 1767 textView.setEllipsize(null); 1768 } 1769 } 1770 1771 private final OnClickListener mPrimaryActionClickListener = new OnClickListener() { 1772 @Override 1773 public void onClick(View view) { 1774 if (mListener == null) return; 1775 final ViewEntry entry = (ViewEntry) view.getTag(); 1776 if (entry == null) return; 1777 entry.click(view, mListener); 1778 } 1779 }; 1780 1781 private final OnClickListener mSecondaryActionClickListener = new OnClickListener() { 1782 @Override 1783 public void onClick(View view) { 1784 if (mListener == null) return; 1785 if (view == null) return; 1786 final ViewEntry entry = (ViewEntry) view.getTag(); 1787 if (entry == null || !(entry instanceof DetailViewEntry)) return; 1788 final DetailViewEntry detailViewEntry = (DetailViewEntry) entry; 1789 final Intent intent = detailViewEntry.secondaryIntent; 1790 if (intent == null) return; 1791 mListener.onItemClicked(intent); 1792 } 1793 }; 1794 1795 @Override 1796 public int getCount() { 1797 return mAllEntries.size(); 1798 } 1799 1800 @Override 1801 public ViewEntry getItem(int position) { 1802 return mAllEntries.get(position); 1803 } 1804 1805 @Override 1806 public int getItemViewType(int position) { 1807 return mAllEntries.get(position).getViewType(); 1808 } 1809 1810 @Override 1811 public int getViewTypeCount() { 1812 return VIEW_TYPE_COUNT; 1813 } 1814 1815 @Override 1816 public long getItemId(int position) { 1817 final ViewEntry entry = mAllEntries.get(position); 1818 if (entry != null) { 1819 return entry.getId(); 1820 } 1821 return -1; 1822 } 1823 1824 @Override 1825 public boolean areAllItemsEnabled() { 1826 // Header will always be an item that is not enabled. 1827 return false; 1828 } 1829 1830 @Override 1831 public boolean isEnabled(int position) { 1832 return getItem(position).isEnabled(); 1833 } 1834 } 1835 1836 @Override 1837 public void onAccountSelectorCancelled() { 1838 } 1839 1840 @Override 1841 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 1842 createCopy(account); 1843 } 1844 1845 private void createCopy(AccountWithDataSet account) { 1846 if (mListener != null) { 1847 mListener.onCreateRawContactRequested(mContactData.getContentValues(), account); 1848 } 1849 } 1850 1851 /** 1852 * Default (fallback) list item click listener. Note the click event for DetailViewEntry is 1853 * caught by individual views in the list item view to distinguish the primary action and the 1854 * secondary action, so this method won't be invoked for that. (The listener is set in the 1855 * bindview in the adapter) 1856 * This listener is used for other kind of entries. 1857 */ 1858 @Override 1859 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1860 if (mListener == null) return; 1861 final ViewEntry entry = mAdapter.getItem(position); 1862 if (entry == null) return; 1863 entry.click(view, mListener); 1864 } 1865 1866 @Override 1867 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 1868 super.onCreateContextMenu(menu, view, menuInfo); 1869 1870 AdapterView.AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; 1871 DetailViewEntry selectedEntry = (DetailViewEntry) mAllEntries.get(info.position); 1872 1873 menu.setHeaderTitle(selectedEntry.data); 1874 menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT, 1875 ContextMenu.NONE, getString(R.string.copy_text)); 1876 1877 String selectedMimeType = selectedEntry.mimetype; 1878 1879 // Defaults to true will only enable the detail to be copied to the clipboard. 1880 boolean isUniqueMimeType = true; 1881 1882 // Only allow primary support for Phone and Email content types 1883 if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 1884 isUniqueMimeType = mIsUniqueNumber; 1885 } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 1886 isUniqueMimeType = mIsUniqueEmail; 1887 } 1888 1889 // Checking for previously set default 1890 if (selectedEntry.isPrimary) { 1891 menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT, 1892 ContextMenu.NONE, getString(R.string.clear_default)); 1893 } else if (!isUniqueMimeType) { 1894 menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT, 1895 ContextMenu.NONE, getString(R.string.set_default)); 1896 } 1897 } 1898 1899 @Override 1900 public boolean onContextItemSelected(MenuItem item) { 1901 AdapterView.AdapterContextMenuInfo menuInfo; 1902 try { 1903 menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 1904 } catch (ClassCastException e) { 1905 Log.e(TAG, "bad menuInfo", e); 1906 return false; 1907 } 1908 1909 switch (item.getItemId()) { 1910 case ContextMenuIds.COPY_TEXT: 1911 copyToClipboard(menuInfo.position); 1912 return true; 1913 case ContextMenuIds.SET_DEFAULT: 1914 setDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position)); 1915 return true; 1916 case ContextMenuIds.CLEAR_DEFAULT: 1917 clearDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position)); 1918 return true; 1919 default: 1920 throw new IllegalArgumentException("Unknown menu option " + item.getItemId()); 1921 } 1922 } 1923 1924 private void setDefaultContactMethod(long id) { 1925 Intent setIntent = ContactSaveService.createSetSuperPrimaryIntent(mContext, id); 1926 mContext.startService(setIntent); 1927 } 1928 1929 private void clearDefaultContactMethod(long id) { 1930 Intent clearIntent = ContactSaveService.createClearPrimaryIntent(mContext, id); 1931 mContext.startService(clearIntent); 1932 } 1933 1934 private void copyToClipboard(int viewEntryPosition) { 1935 // Getting the text to copied 1936 DetailViewEntry detailViewEntry = (DetailViewEntry) mAllEntries.get(viewEntryPosition); 1937 CharSequence textToCopy = detailViewEntry.data; 1938 1939 // Checking for empty string 1940 if (TextUtils.isEmpty(textToCopy)) return; 1941 1942 ClipboardUtils.copyText(getActivity(), detailViewEntry.typeString, textToCopy, true); 1943 } 1944 1945 @Override 1946 public boolean handleKeyDown(int keyCode) { 1947 switch (keyCode) { 1948 case KeyEvent.KEYCODE_CALL: { 1949 try { 1950 ITelephony phone = ITelephony.Stub.asInterface( 1951 ServiceManager.checkService("phone")); 1952 if (phone != null && !phone.isIdle()) { 1953 // Skip out and let the key be handled at a higher level 1954 break; 1955 } 1956 } catch (RemoteException re) { 1957 // Fall through and try to call the contact 1958 } 1959 1960 int index = mListView.getSelectedItemPosition(); 1961 if (index != -1) { 1962 final DetailViewEntry entry = (DetailViewEntry) mAdapter.getItem(index); 1963 if (entry != null && entry.intent != null && 1964 entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) { 1965 mContext.startActivity(entry.intent); 1966 return true; 1967 } 1968 } else if (mPrimaryPhoneUri != null) { 1969 // There isn't anything selected, call the default number 1970 mContext.startActivity(ContactsUtils.getCallIntent(mPrimaryPhoneUri)); 1971 return true; 1972 } 1973 return false; 1974 } 1975 } 1976 1977 return false; 1978 } 1979 1980 /** 1981 * Base class for QuickFixes. QuickFixes quickly fix issues with the Contact without 1982 * requiring the user to go to the editor. Example: Add to My Contacts. 1983 */ 1984 private static abstract class QuickFix { 1985 public abstract boolean isApplicable(); 1986 public abstract String getTitle(); 1987 public abstract void execute(); 1988 } 1989 1990 private class AddToMyContactsQuickFix extends QuickFix { 1991 @Override 1992 public boolean isApplicable() { 1993 // Only local contacts 1994 if (mContactData == null || mContactData.isDirectoryEntry()) return false; 1995 1996 // User profile cannot be added to contacts 1997 if (mContactData.isUserProfile()) return false; 1998 1999 // Only if exactly one raw contact 2000 if (mContactData.getEntities().size() != 1) return false; 2001 2002 // test if the default group is assigned 2003 final List<GroupMetaData> groups = mContactData.getGroupMetaData(); 2004 2005 // For accounts without group support, groups is null 2006 if (groups == null) return false; 2007 2008 // remember the default group id. no default group? bail out early 2009 final long defaultGroupId = getDefaultGroupId(groups); 2010 if (defaultGroupId == -1) return false; 2011 2012 final Entity rawContactEntity = mContactData.getEntities().get(0); 2013 ContentValues rawValues = rawContactEntity.getEntityValues(); 2014 final String accountType = rawValues.getAsString(RawContacts.ACCOUNT_TYPE); 2015 final String dataSet = rawValues.getAsString(RawContacts.DATA_SET); 2016 final AccountTypeManager accountTypes = 2017 AccountTypeManager.getInstance(mContext); 2018 final AccountType type = accountTypes.getAccountType(accountType, dataSet); 2019 // Offline or non-writeable account? Nothing to fix 2020 if (type == null || !type.areContactsWritable()) return false; 2021 2022 // Check whether the contact is in the default group 2023 boolean isInDefaultGroup = false; 2024 for (NamedContentValues subValue : rawContactEntity.getSubValues()) { 2025 final String mimeType = subValue.values.getAsString(Data.MIMETYPE); 2026 2027 if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { 2028 final Long groupId = 2029 subValue.values.getAsLong(GroupMembership.GROUP_ROW_ID); 2030 if (groupId == defaultGroupId) { 2031 isInDefaultGroup = true; 2032 break; 2033 } 2034 } 2035 } 2036 2037 return !isInDefaultGroup; 2038 } 2039 2040 @Override 2041 public String getTitle() { 2042 return getString(R.string.add_to_my_contacts); 2043 } 2044 2045 @Override 2046 public void execute() { 2047 final long defaultGroupId = getDefaultGroupId(mContactData.getGroupMetaData()); 2048 // there should always be a default group (otherwise the button would be invisible), 2049 // but let's be safe here 2050 if (defaultGroupId == -1) return; 2051 2052 // add the group membership to the current state 2053 final EntityDeltaList contactDeltaList = mContactData.createEntityDeltaList(); 2054 final EntityDelta rawContactEntityDelta = contactDeltaList.get(0); 2055 2056 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 2057 final ValuesDelta values = rawContactEntityDelta.getValues(); 2058 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 2059 final String dataSet = values.getAsString(RawContacts.DATA_SET); 2060 final AccountType type = accountTypes.getAccountType(accountType, dataSet); 2061 final DataKind groupMembershipKind = type.getKindForMimetype( 2062 GroupMembership.CONTENT_ITEM_TYPE); 2063 final ValuesDelta entry = EntityModifier.insertChild(rawContactEntityDelta, 2064 groupMembershipKind); 2065 entry.put(GroupMembership.GROUP_ROW_ID, defaultGroupId); 2066 2067 // and fire off the intent. we don't need a callback, as the database listener 2068 // should update the ui 2069 final Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), 2070 contactDeltaList, "", 0, false, getActivity().getClass(), 2071 Intent.ACTION_VIEW, null); 2072 getActivity().startService(intent); 2073 } 2074 } 2075 2076 private class MakeLocalCopyQuickFix extends QuickFix { 2077 @Override 2078 public boolean isApplicable() { 2079 // Not a directory contact? Nothing to fix here 2080 if (mContactData == null || !mContactData.isDirectoryEntry()) return false; 2081 2082 // No export support? Too bad 2083 if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) { 2084 return false; 2085 } 2086 2087 return true; 2088 } 2089 2090 @Override 2091 public String getTitle() { 2092 return getString(R.string.menu_copyContact); 2093 } 2094 2095 @Override 2096 public void execute() { 2097 if (mListener == null) { 2098 return; 2099 } 2100 2101 int exportSupport = mContactData.getDirectoryExportSupport(); 2102 switch (exportSupport) { 2103 case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: { 2104 createCopy(new AccountWithDataSet(mContactData.getDirectoryAccountName(), 2105 mContactData.getDirectoryAccountType(), null)); 2106 break; 2107 } 2108 case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: { 2109 final List<AccountWithDataSet> accounts = 2110 AccountTypeManager.getInstance(mContext).getAccounts(true); 2111 if (accounts.isEmpty()) { 2112 createCopy(null); 2113 return; // Don't show a dialog. 2114 } 2115 2116 // In the common case of a single writable account, auto-select 2117 // it without showing a dialog. 2118 if (accounts.size() == 1) { 2119 createCopy(accounts.get(0)); 2120 return; // Don't show a dialog. 2121 } 2122 2123 SelectAccountDialogFragment.show(getFragmentManager(), 2124 ContactDetailFragment.this, R.string.dialog_new_contact_account, 2125 AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, null); 2126 break; 2127 } 2128 } 2129 } 2130 } 2131 2132 /** 2133 * This class loads the correct padding values for a contact detail item so they can be applied 2134 * dynamically. For example, this supports the case where some detail items can be indented and 2135 * need extra padding. 2136 */ 2137 private static class ViewEntryDimensions { 2138 2139 private final int mWidePaddingLeft; 2140 private final int mPaddingLeft; 2141 private final int mPaddingRight; 2142 private final int mPaddingTop; 2143 private final int mPaddingBottom; 2144 2145 public ViewEntryDimensions(Resources resources) { 2146 mPaddingLeft = resources.getDimensionPixelSize( 2147 R.dimen.detail_item_side_margin); 2148 mPaddingTop = resources.getDimensionPixelSize( 2149 R.dimen.detail_item_vertical_margin); 2150 mWidePaddingLeft = mPaddingLeft + 2151 resources.getDimensionPixelSize(R.dimen.detail_item_icon_margin) + 2152 resources.getDimensionPixelSize(R.dimen.detail_network_icon_size); 2153 mPaddingRight = mPaddingLeft; 2154 mPaddingBottom = mPaddingTop; 2155 } 2156 2157 public int getWidePaddingLeft() { 2158 return mWidePaddingLeft; 2159 } 2160 2161 public int getPaddingLeft() { 2162 return mPaddingLeft; 2163 } 2164 2165 public int getPaddingRight() { 2166 return mPaddingRight; 2167 } 2168 2169 public int getPaddingTop() { 2170 return mPaddingTop; 2171 } 2172 2173 public int getPaddingBottom() { 2174 return mPaddingBottom; 2175 } 2176 } 2177 2178 public static interface Listener { 2179 /** 2180 * User clicked a single item (e.g. mail). The intent passed in could be null. 2181 */ 2182 public void onItemClicked(Intent intent); 2183 2184 /** 2185 * User requested creation of a new contact with the specified values. 2186 * 2187 * @param values ContentValues containing data rows for the new contact. 2188 * @param account Account where the new contact should be created. 2189 */ 2190 public void onCreateRawContactRequested(ArrayList<ContentValues> values, 2191 AccountWithDataSet account); 2192 } 2193 2194 /** 2195 * Adapter for the invitable account types; used for the invitable account type list popup. 2196 */ 2197 private final static class InvitableAccountTypesAdapter extends BaseAdapter { 2198 private final Context mContext; 2199 private final LayoutInflater mInflater; 2200 private final ArrayList<AccountType> mAccountTypes; 2201 2202 public InvitableAccountTypesAdapter(Context context, ContactLoader.Result contactData) { 2203 mContext = context; 2204 mInflater = LayoutInflater.from(context); 2205 final List<AccountType> types = contactData.getInvitableAccountTypes(); 2206 mAccountTypes = new ArrayList<AccountType>(types.size()); 2207 2208 for (int i = 0; i < types.size(); i++) { 2209 mAccountTypes.add(types.get(i)); 2210 } 2211 2212 Collections.sort(mAccountTypes, new AccountType.DisplayLabelComparator(mContext)); 2213 } 2214 2215 @Override 2216 public View getView(int position, View convertView, ViewGroup parent) { 2217 final View resultView = 2218 (convertView != null) ? convertView 2219 : mInflater.inflate(R.layout.account_selector_list_item, parent, false); 2220 2221 final TextView text1 = (TextView)resultView.findViewById(android.R.id.text1); 2222 final TextView text2 = (TextView)resultView.findViewById(android.R.id.text2); 2223 final ImageView icon = (ImageView)resultView.findViewById(android.R.id.icon); 2224 2225 final AccountType accountType = mAccountTypes.get(position); 2226 2227 CharSequence action = accountType.getInviteContactActionLabel(mContext); 2228 CharSequence label = accountType.getDisplayLabel(mContext); 2229 if (TextUtils.isEmpty(action)) { 2230 text1.setText(label); 2231 text2.setVisibility(View.GONE); 2232 } else { 2233 text1.setText(action); 2234 text2.setVisibility(View.VISIBLE); 2235 text2.setText(label); 2236 } 2237 icon.setImageDrawable(accountType.getDisplayIcon(mContext)); 2238 2239 return resultView; 2240 } 2241 2242 @Override 2243 public int getCount() { 2244 return mAccountTypes.size(); 2245 } 2246 2247 @Override 2248 public AccountType getItem(int position) { 2249 return mAccountTypes.get(position); 2250 } 2251 2252 @Override 2253 public long getItemId(int position) { 2254 return position; 2255 } 2256 } 2257 } 2258