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