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.R; 74 import com.android.contacts.TypePrecedence; 75 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener; 76 import com.android.contacts.common.CallUtil; 77 import com.android.contacts.common.ClipboardUtils; 78 import com.android.contacts.common.Collapser; 79 import com.android.contacts.common.ContactsUtils; 80 import com.android.contacts.common.GroupMetaData; 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.common.util.DataStatus; 95 import com.android.contacts.common.util.DateUtils; 96 import com.android.contacts.common.model.Contact; 97 import com.android.contacts.common.model.RawContact; 98 import com.android.contacts.common.model.RawContactDelta; 99 import com.android.contacts.common.model.RawContactDeltaList; 100 import com.android.contacts.common.model.RawContactModifier; 101 import com.android.contacts.common.model.dataitem.DataItem; 102 import com.android.contacts.common.model.dataitem.EmailDataItem; 103 import com.android.contacts.common.model.dataitem.EventDataItem; 104 import com.android.contacts.common.model.dataitem.GroupMembershipDataItem; 105 import com.android.contacts.common.model.dataitem.ImDataItem; 106 import com.android.contacts.common.model.dataitem.NicknameDataItem; 107 import com.android.contacts.common.model.dataitem.NoteDataItem; 108 import com.android.contacts.common.model.dataitem.OrganizationDataItem; 109 import com.android.contacts.common.model.dataitem.PhoneDataItem; 110 import com.android.contacts.common.model.dataitem.RelationDataItem; 111 import com.android.contacts.common.model.dataitem.SipAddressDataItem; 112 import com.android.contacts.common.model.dataitem.StructuredNameDataItem; 113 import com.android.contacts.common.model.dataitem.StructuredPostalDataItem; 114 import com.android.contacts.common.model.dataitem.WebsiteDataItem; 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 // Setup the photo if applicable 416 if (mStaticPhotoContainer != null) { 417 // The presence of a static photo container is not sufficient to determine whether or 418 // not we should show the photo. Check the mShowStaticPhoto flag which can be set by an 419 // outside class depending on screen size, layout, and whether the contact has social 420 // updates or not. 421 if (mShowStaticPhoto) { 422 mStaticPhotoContainer.setVisibility(View.VISIBLE); 423 final ImageView photoView = (ImageView) mStaticPhotoContainer.findViewById( 424 R.id.photo); 425 final boolean expandPhotoOnClick = mContactData.getPhotoUri() != null; 426 final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick( 427 mContext, mContactData, photoView, expandPhotoOnClick); 428 if (mPhotoTouchOverlay != null) { 429 mPhotoTouchOverlay.setVisibility(View.VISIBLE); 430 if (expandPhotoOnClick || mContactData.isWritableContact(mContext)) { 431 mPhotoTouchOverlay.setOnClickListener(listener); 432 } else { 433 mPhotoTouchOverlay.setClickable(false); 434 } 435 } 436 } else { 437 mStaticPhotoContainer.setVisibility(View.GONE); 438 } 439 } 440 441 // Build up the contact entries 442 buildEntries(); 443 444 // Collapse similar data items for select {@link DataKind}s. 445 Collapser.collapseList(mPhoneEntries); 446 Collapser.collapseList(mSmsEntries); 447 Collapser.collapseList(mEmailEntries); 448 Collapser.collapseList(mPostalEntries); 449 Collapser.collapseList(mImEntries); 450 Collapser.collapseList(mEventEntries); 451 Collapser.collapseList(mWebsiteEntries); 452 453 mIsUniqueNumber = mPhoneEntries.size() == 1; 454 mIsUniqueEmail = mEmailEntries.size() == 1; 455 456 // Make one aggregated list of all entries for display to the user. 457 setupFlattenedList(); 458 459 if (mAdapter == null) { 460 mAdapter = new ViewAdapter(); 461 mListView.setAdapter(mAdapter); 462 } 463 464 // Restore {@link ListView} state if applicable because the adapter is now populated. 465 if (mListState != null) { 466 mListView.onRestoreInstanceState(mListState); 467 mListState = null; 468 } 469 470 mAdapter.notifyDataSetChanged(); 471 472 mListView.setEmptyView(mEmptyView); 473 474 configureQuickFix(); 475 476 mView.setVisibility(View.VISIBLE); 477 } 478 479 /* 480 * Sets {@link #mQuickFix} to a useful action and configures the visibility of 481 * {@link #mQuickFixButton} 482 */ 483 private void configureQuickFix() { 484 mQuickFix = null; 485 486 for (QuickFix fix : mPotentialQuickFixes) { 487 if (fix.isApplicable()) { 488 mQuickFix = fix; 489 break; 490 } 491 } 492 493 // Configure the button 494 if (mQuickFix == null) { 495 mQuickFixButton.setVisibility(View.GONE); 496 } else { 497 mQuickFixButton.setVisibility(View.VISIBLE); 498 mQuickFixButton.setText(mQuickFix.getTitle()); 499 } 500 } 501 502 /** @return default group id or -1 if no group or several groups are marked as default */ 503 private long getDefaultGroupId(List<GroupMetaData> groups) { 504 long defaultGroupId = -1; 505 for (GroupMetaData group : groups) { 506 if (group.isDefaultGroup()) { 507 // two default groups? return neither 508 if (defaultGroupId != -1) return -1; 509 defaultGroupId = group.getGroupId(); 510 } 511 } 512 return defaultGroupId; 513 } 514 515 /** 516 * Build up the entries to display on the screen. 517 */ 518 private final void buildEntries() { 519 final boolean hasPhone = PhoneCapabilityTester.isPhone(mContext); 520 final ComponentName smsComponent = PhoneCapabilityTester.getSmsComponent(getContext()); 521 final boolean hasSms = (smsComponent != null); 522 final boolean hasSip = PhoneCapabilityTester.isSipPhone(mContext); 523 524 // Clear out the old entries 525 mAllEntries.clear(); 526 527 mPrimaryPhoneUri = null; 528 529 // Build up method entries 530 if (mContactData == null) { 531 return; 532 } 533 534 ArrayList<String> groups = new ArrayList<String>(); 535 for (RawContact rawContact: mContactData.getRawContacts()) { 536 final long rawContactId = rawContact.getId(); 537 final AccountType accountType = rawContact.getAccountType(mContext); 538 for (DataItem dataItem : rawContact.getDataItems()) { 539 dataItem.setRawContactId(rawContactId); 540 541 if (dataItem.getMimeType() == null) continue; 542 543 if (dataItem instanceof GroupMembershipDataItem) { 544 GroupMembershipDataItem groupMembership = 545 (GroupMembershipDataItem) dataItem; 546 Long groupId = groupMembership.getGroupRowId(); 547 if (groupId != null) { 548 handleGroupMembership(groups, mContactData.getGroupMetaData(), groupId); 549 } 550 continue; 551 } 552 553 final DataKind kind = AccountTypeManager.getInstance(mContext) 554 .getKindOrFallback(accountType, dataItem.getMimeType()); 555 if (kind == null) continue; 556 557 final DetailViewEntry entry = DetailViewEntry.fromValues(mContext, dataItem, 558 mContactData.isDirectoryEntry(), mContactData.getDirectoryId(), kind); 559 entry.maxLines = kind.maxLinesForDisplay; 560 561 final boolean hasData = !TextUtils.isEmpty(entry.data); 562 final boolean isSuperPrimary = dataItem.isSuperPrimary(); 563 564 if (dataItem instanceof StructuredNameDataItem) { 565 // Always ignore the name. It is shown in the header if set 566 } else if (dataItem instanceof PhoneDataItem && hasData) { 567 PhoneDataItem phone = (PhoneDataItem) dataItem; 568 // Build phone entries 569 entry.data = phone.getFormattedPhoneNumber(); 570 final Intent phoneIntent = hasPhone ? 571 CallUtil.getCallIntent(entry.data) : null; 572 Intent smsIntent = null; 573 if (hasSms) { 574 smsIntent = new Intent(Intent.ACTION_SENDTO, 575 Uri.fromParts(CallUtil.SCHEME_SMSTO, entry.data, null)); 576 smsIntent.setComponent(smsComponent); 577 } 578 579 // Configure Icons and Intents. 580 if (hasPhone && hasSms) { 581 entry.intent = phoneIntent; 582 entry.secondaryIntent = smsIntent; 583 entry.secondaryActionIcon = kind.iconAltRes; 584 entry.secondaryActionDescription = 585 ContactDisplayUtils.getSmsLabelResourceId(entry.type); 586 } else if (hasPhone) { 587 entry.intent = phoneIntent; 588 } else if (hasSms) { 589 entry.intent = smsIntent; 590 } else { 591 entry.intent = null; 592 } 593 594 // Remember super-primary phone 595 if (isSuperPrimary) mPrimaryPhoneUri = entry.uri; 596 597 entry.isPrimary = isSuperPrimary; 598 599 // If the entry is a primary entry, then render it first in the view. 600 if (entry.isPrimary) { 601 // add to beginning of list so that this phone number shows up first 602 mPhoneEntries.add(0, entry); 603 } else { 604 // add to end of list 605 mPhoneEntries.add(entry); 606 } 607 608 // Configure the text direction. Phone numbers should be displayed LTR 609 // regardless of what locale the device is in. 610 entry.textDirection = View.TEXT_DIRECTION_LTR; 611 } else if (dataItem instanceof EmailDataItem && hasData) { 612 // Build email entries 613 entry.intent = new Intent(Intent.ACTION_SENDTO, 614 Uri.fromParts(CallUtil.SCHEME_MAILTO, entry.data, null)); 615 entry.isPrimary = isSuperPrimary; 616 // If entry is a primary entry, then render it first in the view. 617 if (entry.isPrimary) { 618 mEmailEntries.add(0, entry); 619 } else { 620 mEmailEntries.add(entry); 621 } 622 623 // When Email rows have status, create additional Im row 624 final DataStatus status = mContactData.getStatuses().get(entry.id); 625 if (status != null) { 626 EmailDataItem email = (EmailDataItem) dataItem; 627 ImDataItem im = ImDataItem.createFromEmail(email); 628 629 final DetailViewEntry imEntry = DetailViewEntry.fromValues(mContext, im, 630 mContactData.isDirectoryEntry(), mContactData.getDirectoryId(), 631 kind); 632 buildImActions(mContext, imEntry, im); 633 imEntry.setPresence(status.getPresence()); 634 imEntry.maxLines = kind.maxLinesForDisplay; 635 mImEntries.add(imEntry); 636 } 637 } else if (dataItem instanceof StructuredPostalDataItem && hasData) { 638 // Build postal entries 639 entry.intent = StructuredPostalUtils.getViewPostalAddressIntent(entry.data); 640 mPostalEntries.add(entry); 641 } else if (dataItem instanceof ImDataItem && hasData) { 642 // Build IM entries 643 buildImActions(mContext, entry, (ImDataItem) dataItem); 644 645 // Apply presence when available 646 final DataStatus status = mContactData.getStatuses().get(entry.id); 647 if (status != null) { 648 entry.setPresence(status.getPresence()); 649 } 650 mImEntries.add(entry); 651 } else if (dataItem instanceof OrganizationDataItem) { 652 // Organizations are not shown. The first one is shown in the header 653 // and subsequent ones are not supported anymore 654 } else if (dataItem instanceof NicknameDataItem && hasData) { 655 // Build nickname entries 656 final boolean isNameRawContact = 657 (mContactData.getNameRawContactId() == rawContactId); 658 659 final boolean duplicatesTitle = 660 isNameRawContact 661 && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; 662 663 if (!duplicatesTitle) { 664 entry.uri = null; 665 mNicknameEntries.add(entry); 666 } 667 } else if (dataItem instanceof NoteDataItem && hasData) { 668 // Build note entries 669 entry.uri = null; 670 mNoteEntries.add(entry); 671 } else if (dataItem instanceof WebsiteDataItem && hasData) { 672 // Build Website entries 673 entry.uri = null; 674 try { 675 WebAddress webAddress = new WebAddress(entry.data); 676 entry.intent = new Intent(Intent.ACTION_VIEW, 677 Uri.parse(webAddress.toString())); 678 } catch (ParseException e) { 679 Log.e(TAG, "Couldn't parse website: " + entry.data); 680 } 681 mWebsiteEntries.add(entry); 682 } else if (dataItem instanceof SipAddressDataItem && hasData) { 683 // Build SipAddress entries 684 entry.uri = null; 685 if (hasSip) { 686 entry.intent = CallUtil.getCallIntent( 687 Uri.fromParts(CallUtil.SCHEME_SIP, entry.data, null)); 688 } else { 689 entry.intent = null; 690 } 691 mSipEntries.add(entry); 692 // TODO: Now that SipAddress is in its own list of entries 693 // (instead of grouped in mOtherEntries), consider 694 // repositioning it right under the phone number. 695 // (Then, we'd also update FallbackAccountType.java to set 696 // secondary=false for this field, and tweak the weight 697 // of its DataKind.) 698 } else if (dataItem instanceof EventDataItem && hasData) { 699 final Calendar cal = DateUtils.parseDate(entry.data, false); 700 if (cal != null) { 701 final Date nextAnniversary = 702 DateUtils.getNextAnnualDate(cal); 703 final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon(); 704 builder.appendPath("time"); 705 ContentUris.appendId(builder, nextAnniversary.getTime()); 706 entry.intent = new Intent(Intent.ACTION_VIEW).setData(builder.build()); 707 } 708 entry.data = DateUtils.formatDate(mContext, entry.data); 709 entry.uri = null; 710 mEventEntries.add(entry); 711 } else if (dataItem instanceof RelationDataItem && hasData) { 712 entry.intent = new Intent(Intent.ACTION_SEARCH); 713 entry.intent.putExtra(SearchManager.QUERY, entry.data); 714 entry.intent.setType(Contacts.CONTENT_TYPE); 715 mRelationEntries.add(entry); 716 } else { 717 // Handle showing custom rows 718 entry.intent = new Intent(Intent.ACTION_VIEW); 719 entry.intent.setDataAndType(entry.uri, entry.mimetype); 720 721 entry.data = dataItem.buildDataString(getContext(), kind); 722 723 if (!TextUtils.isEmpty(entry.data)) { 724 // If the account type exists in the hash map, add it as another entry for 725 // that account type 726 if (mOtherEntriesMap.containsKey(accountType)) { 727 List<DetailViewEntry> listEntries = mOtherEntriesMap.get(accountType); 728 listEntries.add(entry); 729 } else { 730 // Otherwise create a new list with the entry and add it to the hash map 731 List<DetailViewEntry> listEntries = new ArrayList<DetailViewEntry>(); 732 listEntries.add(entry); 733 mOtherEntriesMap.put(accountType, listEntries); 734 } 735 } 736 } 737 } 738 } 739 740 if (!groups.isEmpty()) { 741 DetailViewEntry entry = new DetailViewEntry(); 742 Collections.sort(groups); 743 StringBuilder sb = new StringBuilder(); 744 int size = groups.size(); 745 for (int i = 0; i < size; i++) { 746 if (i != 0) { 747 sb.append(", "); 748 } 749 sb.append(groups.get(i)); 750 } 751 entry.mimetype = GroupMembership.MIMETYPE; 752 entry.kind = mContext.getString(R.string.groupsLabel); 753 entry.data = sb.toString(); 754 mGroupEntries.add(entry); 755 } 756 } 757 758 /** 759 * Collapse all contact detail entries into one aggregated list with a {@link HeaderViewEntry} 760 * at the top. 761 */ 762 private void setupFlattenedList() { 763 // All contacts should have a header view (even if there is no data for the contact). 764 mAllEntries.add(new HeaderViewEntry()); 765 766 addPhoneticName(); 767 768 flattenList(mPhoneEntries); 769 flattenList(mSmsEntries); 770 flattenList(mEmailEntries); 771 flattenList(mImEntries); 772 flattenList(mNicknameEntries); 773 flattenList(mWebsiteEntries); 774 775 addNetworks(); 776 777 flattenList(mSipEntries); 778 flattenList(mPostalEntries); 779 flattenList(mEventEntries); 780 flattenList(mGroupEntries); 781 flattenList(mRelationEntries); 782 flattenList(mNoteEntries); 783 } 784 785 /** 786 * Add phonetic name (if applicable) to the aggregated list of contact details. This has to be 787 * done manually because phonetic name doesn't have a mimetype or action intent. 788 */ 789 private void addPhoneticName() { 790 String phoneticName = ContactDetailDisplayUtils.getPhoneticName(mContext, mContactData); 791 if (TextUtils.isEmpty(phoneticName)) { 792 return; 793 } 794 795 // Add a title 796 String phoneticNameKindTitle = mContext.getString(R.string.name_phonetic); 797 mAllEntries.add(new KindTitleViewEntry(phoneticNameKindTitle.toUpperCase())); 798 799 // Add the phonetic name 800 final DetailViewEntry entry = new DetailViewEntry(); 801 entry.kind = phoneticNameKindTitle; 802 entry.data = phoneticName; 803 mAllEntries.add(entry); 804 } 805 806 /** 807 * Add attribution and other third-party entries (if applicable) under the "networks" section 808 * of the aggregated list of contact details. This has to be done manually because the 809 * attribution does not have a mimetype and the third-party entries don't have actually belong 810 * to the same {@link DataKind}. 811 */ 812 private void addNetworks() { 813 String attribution = ContactDetailDisplayUtils.getAttribution(mContext, mContactData); 814 boolean hasAttribution = !TextUtils.isEmpty(attribution); 815 int networksCount = mOtherEntriesMap.keySet().size(); 816 817 // Note: invitableCount will always be 0 for me profile. (ContactLoader won't set 818 // invitable types for me profile.) 819 int invitableCount = mContactData.getInvitableAccountTypes().size(); 820 if (!hasAttribution && networksCount == 0 && invitableCount == 0) { 821 return; 822 } 823 824 // Add a title 825 String networkKindTitle = mContext.getString(R.string.connections); 826 mAllEntries.add(new KindTitleViewEntry(networkKindTitle.toUpperCase())); 827 828 // Add the attribution if applicable 829 if (hasAttribution) { 830 final DetailViewEntry entry = new DetailViewEntry(); 831 entry.kind = networkKindTitle; 832 entry.data = attribution; 833 mAllEntries.add(entry); 834 835 // Add a divider below the attribution if there are network details that will follow 836 if (networksCount > 0) { 837 mAllEntries.add(new SeparatorViewEntry()); 838 } 839 } 840 841 // Add the other entries from third parties 842 for (AccountType accountType : mOtherEntriesMap.keySet()) { 843 844 // Add a title for each third party app 845 mAllEntries.add(new NetworkTitleViewEntry(mContext, accountType)); 846 847 for (DetailViewEntry detailEntry : mOtherEntriesMap.get(accountType)) { 848 // Add indented separator 849 SeparatorViewEntry separatorEntry = new SeparatorViewEntry(); 850 separatorEntry.setIsInSubSection(true); 851 mAllEntries.add(separatorEntry); 852 853 // Add indented detail 854 detailEntry.setIsInSubSection(true); 855 mAllEntries.add(detailEntry); 856 } 857 } 858 859 mOtherEntriesMap.clear(); 860 861 // Add the "More networks" button, which opens the invitable account type list popup. 862 if (invitableCount > 0) { 863 addMoreNetworks(); 864 } 865 } 866 867 /** 868 * Add the "More networks" entry. When clicked, show a popup containing a list of invitable 869 * account types. 870 */ 871 private void addMoreNetworks() { 872 // First, prepare for the popup. 873 874 // Adapter for the list popup. 875 final InvitableAccountTypesAdapter popupAdapter = new InvitableAccountTypesAdapter(mContext, 876 mContactData); 877 878 // Listener called when a popup item is clicked. 879 final AdapterView.OnItemClickListener popupItemListener 880 = new AdapterView.OnItemClickListener() { 881 @Override 882 public void onItemClick(AdapterView<?> parent, View view, int position, 883 long id) { 884 if (mListener != null && mContactData != null) { 885 mListener.onItemClicked(MoreContactUtils.getInvitableIntent( 886 popupAdapter.getItem(position) /* account type */, 887 mContactData.getLookupUri())); 888 } 889 } 890 }; 891 892 // Then create the click listener for the "More network" entry. Open the popup. 893 View.OnClickListener onClickListener = new OnClickListener() { 894 @Override 895 public void onClick(View v) { 896 showListPopup(v, popupAdapter, popupItemListener); 897 } 898 }; 899 900 // Finally create the entry. 901 mAllEntries.add(new AddConnectionViewEntry(mContext, onClickListener)); 902 } 903 904 /** 905 * Iterate through {@link DetailViewEntry} in the given list and add it to a list of all 906 * entries. Add a {@link KindTitleViewEntry} at the start if the length of the list is not 0. 907 * Add {@link SeparatorViewEntry}s as dividers as appropriate. Clear the original list. 908 */ 909 private void flattenList(ArrayList<DetailViewEntry> entries) { 910 int count = entries.size(); 911 912 // Add a title for this kind by extracting the kind from the first entry 913 if (count > 0) { 914 String kind = entries.get(0).kind; 915 mAllEntries.add(new KindTitleViewEntry(kind.toUpperCase())); 916 } 917 918 // Add all the data entries for this kind 919 for (int i = 0; i < count; i++) { 920 // For all entries except the first one, add a divider above the entry 921 if (i != 0) { 922 mAllEntries.add(new SeparatorViewEntry()); 923 } 924 mAllEntries.add(entries.get(i)); 925 } 926 927 // Clear old list because it's not needed anymore. 928 entries.clear(); 929 } 930 931 /** 932 * Maps group ID to the corresponding group name, collapses all synonymous groups. 933 * Ignores default groups (e.g. My Contacts) and favorites groups. 934 */ 935 private void handleGroupMembership( 936 ArrayList<String> groups, List<GroupMetaData> groupMetaData, long groupId) { 937 if (groupMetaData == null) { 938 return; 939 } 940 941 for (GroupMetaData group : groupMetaData) { 942 if (group.getGroupId() == groupId) { 943 if (!group.isDefaultGroup() && !group.isFavorites()) { 944 String title = group.getTitle(); 945 if (!TextUtils.isEmpty(title) && !groups.contains(title)) { 946 groups.add(title); 947 } 948 } 949 break; 950 } 951 } 952 } 953 954 /** 955 * Writes the Instant Messaging action into the given entry value. 956 */ 957 @VisibleForTesting 958 public static void buildImActions(Context context, DetailViewEntry entry, 959 ImDataItem im) { 960 final boolean isEmail = im.isCreatedFromEmail(); 961 962 if (!isEmail && !im.isProtocolValid()) { 963 return; 964 } 965 966 final String data = im.getData(); 967 if (TextUtils.isEmpty(data)) { 968 return; 969 } 970 971 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); 972 973 if (protocol == Im.PROTOCOL_GOOGLE_TALK) { 974 final int chatCapability = im.getChatCapability(); 975 entry.chatCapability = chatCapability; 976 entry.typeString = Im.getProtocolLabel(context.getResources(), Im.PROTOCOL_GOOGLE_TALK, 977 null).toString(); 978 if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { 979 entry.intent = 980 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); 981 entry.secondaryIntent = 982 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); 983 } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { 984 // Allow Talking and Texting 985 entry.intent = 986 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); 987 entry.secondaryIntent = 988 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); 989 } else { 990 entry.intent = 991 new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); 992 } 993 } else { 994 // Build an IM Intent 995 final Intent imIntent = getCustomIMIntent(im, protocol); 996 if (imIntent != null && 997 PhoneCapabilityTester.isIntentRegistered(context, imIntent)) { 998 entry.intent = imIntent; 999 } 1000 } 1001 } 1002 1003 @VisibleForTesting 1004 public static Intent getCustomIMIntent(ImDataItem im, int protocol) { 1005 String host = im.getCustomProtocol(); 1006 final String data = im.getData(); 1007 if (TextUtils.isEmpty(data)) { 1008 return null; 1009 } 1010 if (protocol != Im.PROTOCOL_CUSTOM) { 1011 // Try bringing in a well-known host for specific protocols 1012 host = ContactsUtils.lookupProviderNameFromId(protocol); 1013 } 1014 if (TextUtils.isEmpty(host)) { 1015 return null; 1016 } 1017 final String authority = host.toLowerCase(); 1018 final Uri imUri = new Uri.Builder().scheme(CallUtil.SCHEME_IMTO).authority( 1019 authority).appendPath(data).build(); 1020 final Intent intent = new Intent(Intent.ACTION_SENDTO, imUri); 1021 return intent; 1022 } 1023 1024 /** 1025 * Show a list popup. Used for "popup-able" entry, such as "More networks". 1026 */ 1027 private void showListPopup(View anchorView, ListAdapter adapter, 1028 final AdapterView.OnItemClickListener onItemClickListener) { 1029 dismissPopupIfShown(); 1030 mPopup = new ListPopupWindow(mContext, null); 1031 mPopup.setAnchorView(anchorView); 1032 mPopup.setWidth(anchorView.getWidth()); 1033 mPopup.setAdapter(adapter); 1034 mPopup.setModal(true); 1035 1036 // We need to wrap the passed onItemClickListener here, so that we can dismiss() the 1037 // popup afterwards. Otherwise we could directly use the passed listener. 1038 mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1039 @Override 1040 public void onItemClick(AdapterView<?> parent, View view, int position, 1041 long id) { 1042 onItemClickListener.onItemClick(parent, view, position, id); 1043 dismissPopupIfShown(); 1044 } 1045 }); 1046 mPopup.show(); 1047 } 1048 1049 private void dismissPopupIfShown() { 1050 UiClosables.closeQuietly(mPopup); 1051 mPopup = null; 1052 } 1053 1054 /** 1055 * Base class for an item in the {@link ViewAdapter} list of data, which is 1056 * supplied to the {@link ListView}. 1057 */ 1058 static class ViewEntry { 1059 private final int viewTypeForAdapter; 1060 protected long id = -1; 1061 /** Whether or not the entry can be focused on or not. */ 1062 protected boolean isEnabled = false; 1063 1064 ViewEntry(int viewType) { 1065 viewTypeForAdapter = viewType; 1066 } 1067 1068 int getViewType() { 1069 return viewTypeForAdapter; 1070 } 1071 1072 long getId() { 1073 return id; 1074 } 1075 1076 boolean isEnabled(){ 1077 return isEnabled; 1078 } 1079 1080 /** 1081 * Called when the entry is clicked. Only {@link #isEnabled} entries can get clicked. 1082 * 1083 * @param clickedView {@link View} that was clicked (Used, for example, as the anchor view 1084 * for a popup.) 1085 * @param fragmentListener {@link Listener} set to {@link ContactDetailFragment} 1086 */ 1087 public void click(View clickedView, Listener fragmentListener) { 1088 } 1089 } 1090 1091 /** 1092 * Header item in the {@link ViewAdapter} list of data. 1093 */ 1094 private static class HeaderViewEntry extends ViewEntry { 1095 1096 HeaderViewEntry() { 1097 super(ViewAdapter.VIEW_TYPE_HEADER_ENTRY); 1098 } 1099 1100 } 1101 1102 /** 1103 * Separator between items of the same {@link DataKind} in the 1104 * {@link ViewAdapter} list of data. 1105 */ 1106 private static class SeparatorViewEntry extends ViewEntry { 1107 1108 /** 1109 * Whether or not the entry is in a subsection (if true then the contents will be indented 1110 * to the right) 1111 */ 1112 private boolean mIsInSubSection = false; 1113 1114 SeparatorViewEntry() { 1115 super(ViewAdapter.VIEW_TYPE_SEPARATOR_ENTRY); 1116 } 1117 1118 public void setIsInSubSection(boolean isInSubSection) { 1119 mIsInSubSection = isInSubSection; 1120 } 1121 1122 public boolean isInSubSection() { 1123 return mIsInSubSection; 1124 } 1125 } 1126 1127 /** 1128 * Title entry for items of the same {@link DataKind} in the 1129 * {@link ViewAdapter} list of data. 1130 */ 1131 private static class KindTitleViewEntry extends ViewEntry { 1132 1133 private final String mTitle; 1134 1135 KindTitleViewEntry(String titleText) { 1136 super(ViewAdapter.VIEW_TYPE_KIND_TITLE_ENTRY); 1137 mTitle = titleText; 1138 } 1139 1140 public String getTitle() { 1141 return mTitle; 1142 } 1143 } 1144 1145 /** 1146 * A title for a section of contact details from a single 3rd party network. 1147 */ 1148 private static class NetworkTitleViewEntry extends ViewEntry { 1149 private final Drawable mIcon; 1150 private final CharSequence mLabel; 1151 1152 public NetworkTitleViewEntry(Context context, AccountType type) { 1153 super(ViewAdapter.VIEW_TYPE_NETWORK_TITLE_ENTRY); 1154 this.mIcon = type.getDisplayIcon(context); 1155 this.mLabel = type.getDisplayLabel(context); 1156 this.isEnabled = false; 1157 } 1158 1159 public Drawable getIcon() { 1160 return mIcon; 1161 } 1162 1163 public CharSequence getLabel() { 1164 return mLabel; 1165 } 1166 } 1167 1168 /** 1169 * This is used for the "Add Connections" entry. 1170 */ 1171 private static class AddConnectionViewEntry extends ViewEntry { 1172 private final Drawable mIcon; 1173 private final CharSequence mLabel; 1174 private final View.OnClickListener mOnClickListener; 1175 1176 private AddConnectionViewEntry(Context context, View.OnClickListener onClickListener) { 1177 super(ViewAdapter.VIEW_TYPE_ADD_CONNECTION_ENTRY); 1178 this.mIcon = context.getResources().getDrawable( 1179 R.drawable.ic_menu_add_field_holo_light); 1180 this.mLabel = context.getString(R.string.add_connection_button); 1181 this.mOnClickListener = onClickListener; 1182 this.isEnabled = true; 1183 } 1184 1185 @Override 1186 public void click(View clickedView, Listener fragmentListener) { 1187 if (mOnClickListener == null) return; 1188 mOnClickListener.onClick(clickedView); 1189 } 1190 1191 public Drawable getIcon() { 1192 return mIcon; 1193 } 1194 1195 public CharSequence getLabel() { 1196 return mLabel; 1197 } 1198 } 1199 1200 /** 1201 * An item with a single detail for a contact in the {@link ViewAdapter} 1202 * list of data. 1203 */ 1204 static class DetailViewEntry extends ViewEntry implements Collapsible<DetailViewEntry> { 1205 // TODO: Make getters/setters for these fields 1206 public int type = -1; 1207 public String kind; 1208 public String typeString; 1209 public String data; 1210 public Uri uri; 1211 public int maxLines = 1; 1212 public int textDirection = TEXT_DIRECTION_UNDEFINED; 1213 public String mimetype; 1214 1215 public Context context = null; 1216 public boolean isPrimary = false; 1217 public int secondaryActionIcon = -1; 1218 public int secondaryActionDescription = -1; 1219 public Intent intent; 1220 public Intent secondaryIntent = null; 1221 public ArrayList<Long> ids = new ArrayList<Long>(); 1222 public int collapseCount = 0; 1223 1224 public int presence = -1; 1225 public int chatCapability = 0; 1226 1227 private boolean mIsInSubSection = false; 1228 1229 @Override 1230 public String toString() { 1231 return Objects.toStringHelper(this) 1232 .add("type", type) 1233 .add("kind", kind) 1234 .add("typeString", typeString) 1235 .add("data", data) 1236 .add("uri", uri) 1237 .add("maxLines", maxLines) 1238 .add("mimetype", mimetype) 1239 .add("context", context) 1240 .add("isPrimary", isPrimary) 1241 .add("secondaryActionIcon", secondaryActionIcon) 1242 .add("secondaryActionDescription", secondaryActionDescription) 1243 .add("intent", intent) 1244 .add("secondaryIntent", secondaryIntent) 1245 .add("ids", ids) 1246 .add("collapseCount", collapseCount) 1247 .add("presence", presence) 1248 .add("chatCapability", chatCapability) 1249 .add("mIsInSubSection", mIsInSubSection) 1250 .toString(); 1251 } 1252 1253 DetailViewEntry() { 1254 super(ViewAdapter.VIEW_TYPE_DETAIL_ENTRY); 1255 isEnabled = true; 1256 } 1257 1258 /** 1259 * Build new {@link DetailViewEntry} and populate from the given values. 1260 */ 1261 public static DetailViewEntry fromValues(Context context, DataItem item, 1262 boolean isDirectoryEntry, long directoryId, DataKind dataKind) { 1263 final DetailViewEntry entry = new DetailViewEntry(); 1264 entry.id = item.getId(); 1265 entry.context = context; 1266 entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id); 1267 if (isDirectoryEntry) { 1268 entry.uri = entry.uri.buildUpon().appendQueryParameter( 1269 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build(); 1270 } 1271 entry.mimetype = item.getMimeType(); 1272 entry.kind = dataKind.getKindString(context); 1273 entry.data = item.buildDataString(context, dataKind); 1274 1275 if (item.hasKindTypeColumn(dataKind)) { 1276 entry.type = item.getKindTypeColumn(dataKind); 1277 1278 // get type string 1279 entry.typeString = ""; 1280 for (EditType type : dataKind.typeList) { 1281 if (type.rawValue == entry.type) { 1282 if (type.customColumn == null) { 1283 // Non-custom type. Get its description from the resource 1284 entry.typeString = context.getString(type.labelRes); 1285 } else { 1286 // Custom type. Read it from the database 1287 entry.typeString = 1288 item.getContentValues().getAsString(type.customColumn); 1289 } 1290 break; 1291 } 1292 } 1293 } else { 1294 entry.typeString = ""; 1295 } 1296 1297 return entry; 1298 } 1299 1300 public void setPresence(int presence) { 1301 this.presence = presence; 1302 } 1303 1304 public void setIsInSubSection(boolean isInSubSection) { 1305 mIsInSubSection = isInSubSection; 1306 } 1307 1308 public boolean isInSubSection() { 1309 return mIsInSubSection; 1310 } 1311 1312 @Override 1313 public void collapseWith(DetailViewEntry entry) { 1314 // Choose the label associated with the highest type precedence. 1315 if (TypePrecedence.getTypePrecedence(mimetype, type) 1316 > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) { 1317 type = entry.type; 1318 kind = entry.kind; 1319 typeString = entry.typeString; 1320 } 1321 1322 // Choose the max of the maxLines and maxLabelLines values. 1323 maxLines = Math.max(maxLines, entry.maxLines); 1324 1325 // Choose the presence with the highest precedence. 1326 if (StatusUpdates.getPresencePrecedence(presence) 1327 < StatusUpdates.getPresencePrecedence(entry.presence)) { 1328 presence = entry.presence; 1329 } 1330 1331 // If any of the collapsed entries are primary make the whole thing primary. 1332 isPrimary = entry.isPrimary ? true : isPrimary; 1333 1334 // uri, and contactdId, shouldn't make a difference. Just keep the original. 1335 1336 // Keep track of all the ids that have been collapsed with this one. 1337 ids.add(entry.getId()); 1338 collapseCount++; 1339 } 1340 1341 @Override 1342 public boolean shouldCollapseWith(DetailViewEntry entry) { 1343 if (entry == null) { 1344 return false; 1345 } 1346 1347 if (!MoreContactUtils.shouldCollapse(mimetype, data, entry.mimetype, entry.data)) { 1348 return false; 1349 } 1350 1351 if (!TextUtils.equals(mimetype, entry.mimetype) 1352 || !ContactsUtils.areIntentActionEqual(intent, entry.intent) 1353 || !ContactsUtils.areIntentActionEqual( 1354 secondaryIntent, entry.secondaryIntent)) { 1355 return false; 1356 } 1357 1358 return true; 1359 } 1360 1361 @Override 1362 public void click(View clickedView, Listener fragmentListener) { 1363 if (fragmentListener == null || intent == null) return; 1364 fragmentListener.onItemClicked(intent); 1365 } 1366 } 1367 1368 /** 1369 * Cache of the children views for a view that displays a header view entry. 1370 */ 1371 private static class HeaderViewCache { 1372 public final TextView displayNameView; 1373 public final TextView companyView; 1374 public final ImageView photoView; 1375 public final View photoOverlayView; 1376 public final ImageView starredView; 1377 public final int layoutResourceId; 1378 1379 public HeaderViewCache(View view, int layoutResourceInflated) { 1380 displayNameView = (TextView) view.findViewById(R.id.name); 1381 companyView = (TextView) view.findViewById(R.id.company); 1382 photoView = (ImageView) view.findViewById(R.id.photo); 1383 photoOverlayView = view.findViewById(R.id.photo_touch_intercept_overlay); 1384 starredView = (ImageView) view.findViewById(R.id.star); 1385 layoutResourceId = layoutResourceInflated; 1386 } 1387 1388 public void enablePhotoOverlay(OnClickListener listener) { 1389 if (photoOverlayView != null) { 1390 photoOverlayView.setOnClickListener(listener); 1391 photoOverlayView.setVisibility(View.VISIBLE); 1392 } 1393 } 1394 } 1395 1396 private static class KindTitleViewCache { 1397 public final TextView titleView; 1398 1399 public KindTitleViewCache(View view) { 1400 titleView = (TextView)view.findViewById(R.id.title); 1401 } 1402 } 1403 1404 /** 1405 * Cache of the children views for a view that displays a {@link NetworkTitleViewEntry} 1406 */ 1407 private static class NetworkTitleViewCache { 1408 public final TextView name; 1409 public final ImageView icon; 1410 1411 public NetworkTitleViewCache(View view) { 1412 name = (TextView) view.findViewById(R.id.network_title); 1413 icon = (ImageView) view.findViewById(R.id.network_icon); 1414 } 1415 } 1416 1417 /** 1418 * Cache of the children views for a view that displays a {@link AddConnectionViewEntry} 1419 */ 1420 private static class AddConnectionViewCache { 1421 public final TextView name; 1422 public final ImageView icon; 1423 public final View primaryActionView; 1424 1425 public AddConnectionViewCache(View view) { 1426 name = (TextView) view.findViewById(R.id.add_connection_label); 1427 icon = (ImageView) view.findViewById(R.id.add_connection_icon); 1428 primaryActionView = view.findViewById(R.id.primary_action_view); 1429 } 1430 } 1431 1432 /** 1433 * Cache of the children views of a contact detail entry represented by a 1434 * {@link DetailViewEntry} 1435 */ 1436 private static class DetailViewCache { 1437 public final TextView type; 1438 public final TextView data; 1439 public final ImageView presenceIcon; 1440 public final ImageView secondaryActionButton; 1441 public final View actionsViewContainer; 1442 public final View primaryActionView; 1443 public final View secondaryActionViewContainer; 1444 public final View secondaryActionDivider; 1445 public final View primaryIndicator; 1446 1447 public DetailViewCache(View view, 1448 OnClickListener primaryActionClickListener, 1449 OnClickListener secondaryActionClickListener) { 1450 type = (TextView) view.findViewById(R.id.type); 1451 data = (TextView) view.findViewById(R.id.data); 1452 primaryIndicator = view.findViewById(R.id.primary_indicator); 1453 presenceIcon = (ImageView) view.findViewById(R.id.presence_icon); 1454 1455 actionsViewContainer = view.findViewById(R.id.actions_view_container); 1456 actionsViewContainer.setOnClickListener(primaryActionClickListener); 1457 primaryActionView = view.findViewById(R.id.primary_action_view); 1458 1459 secondaryActionViewContainer = view.findViewById( 1460 R.id.secondary_action_view_container); 1461 secondaryActionViewContainer.setOnClickListener( 1462 secondaryActionClickListener); 1463 secondaryActionButton = (ImageView) view.findViewById( 1464 R.id.secondary_action_button); 1465 1466 secondaryActionDivider = view.findViewById(R.id.vertical_divider); 1467 } 1468 } 1469 1470 private final class ViewAdapter extends BaseAdapter { 1471 1472 public static final int VIEW_TYPE_DETAIL_ENTRY = 0; 1473 public static final int VIEW_TYPE_HEADER_ENTRY = 1; 1474 public static final int VIEW_TYPE_KIND_TITLE_ENTRY = 2; 1475 public static final int VIEW_TYPE_NETWORK_TITLE_ENTRY = 3; 1476 public static final int VIEW_TYPE_ADD_CONNECTION_ENTRY = 4; 1477 public static final int VIEW_TYPE_SEPARATOR_ENTRY = 5; 1478 private static final int VIEW_TYPE_COUNT = 6; 1479 1480 @Override 1481 public View getView(int position, View convertView, ViewGroup parent) { 1482 switch (getItemViewType(position)) { 1483 case VIEW_TYPE_HEADER_ENTRY: 1484 return getHeaderEntryView(convertView, parent); 1485 case VIEW_TYPE_SEPARATOR_ENTRY: 1486 return getSeparatorEntryView(position, convertView, parent); 1487 case VIEW_TYPE_KIND_TITLE_ENTRY: 1488 return getKindTitleEntryView(position, convertView, parent); 1489 case VIEW_TYPE_DETAIL_ENTRY: 1490 return getDetailEntryView(position, convertView, parent); 1491 case VIEW_TYPE_NETWORK_TITLE_ENTRY: 1492 return getNetworkTitleEntryView(position, convertView, parent); 1493 case VIEW_TYPE_ADD_CONNECTION_ENTRY: 1494 return getAddConnectionEntryView(position, convertView, parent); 1495 default: 1496 throw new IllegalStateException("Invalid view type ID " + 1497 getItemViewType(position)); 1498 } 1499 } 1500 1501 private View getHeaderEntryView(View convertView, ViewGroup parent) { 1502 final int desiredLayoutResourceId = R.layout.detail_header_contact_without_updates; 1503 View result = null; 1504 HeaderViewCache viewCache = null; 1505 1506 // Only use convertView if it has the same layout resource ID as the one desired 1507 // (the two can be different on wide 2-pane screens where the detail fragment is reused 1508 // for many different contacts that do and do not have social updates). 1509 if (convertView != null) { 1510 viewCache = (HeaderViewCache) convertView.getTag(); 1511 if (viewCache.layoutResourceId == desiredLayoutResourceId) { 1512 result = convertView; 1513 } 1514 } 1515 1516 // Otherwise inflate a new header view and create a new view cache. 1517 if (result == null) { 1518 result = mInflater.inflate(desiredLayoutResourceId, parent, false); 1519 viewCache = new HeaderViewCache(result, desiredLayoutResourceId); 1520 result.setTag(viewCache); 1521 } 1522 1523 ContactDetailDisplayUtils.setDisplayName(mContext, mContactData, 1524 viewCache.displayNameView); 1525 ContactDetailDisplayUtils.setCompanyName(mContext, mContactData, viewCache.companyView); 1526 1527 // Set the photo if it should be displayed 1528 if (viewCache.photoView != null) { 1529 final boolean expandOnClick = mContactData.getPhotoUri() != null; 1530 final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick( 1531 mContext, mContactData, viewCache.photoView, expandOnClick); 1532 1533 if (expandOnClick || mContactData.isWritableContact(mContext)) { 1534 viewCache.enablePhotoOverlay(listener); 1535 } 1536 } 1537 1538 // Set the starred state if it should be displayed 1539 final ImageView favoritesStar = viewCache.starredView; 1540 if (favoritesStar != null) { 1541 ContactDetailDisplayUtils.configureStarredImageView(favoritesStar, 1542 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1543 mContactData.getStarred()); 1544 final Uri lookupUri = mContactData.getLookupUri(); 1545 favoritesStar.setOnClickListener(new OnClickListener() { 1546 @Override 1547 public void onClick(View v) { 1548 // Toggle "starred" state 1549 // Make sure there is a contact 1550 if (lookupUri != null) { 1551 // Read the current starred value from the UI instead of using the last 1552 // loaded state. This allows rapid tapping without writing the same 1553 // value several times 1554 final Object tag = favoritesStar.getTag(); 1555 final boolean isStarred = tag == null 1556 ? false : (Boolean) favoritesStar.getTag(); 1557 1558 // To improve responsiveness, swap out the picture (and tag) in the UI 1559 // already 1560 ContactDetailDisplayUtils.configureStarredImageView(favoritesStar, 1561 mContactData.isDirectoryEntry(), mContactData.isUserProfile(), 1562 !isStarred); 1563 1564 // Now perform the real save 1565 Intent intent = ContactSaveService.createSetStarredIntent( 1566 getContext(), lookupUri, !isStarred); 1567 getContext().startService(intent); 1568 } 1569 } 1570 }); 1571 } 1572 1573 return result; 1574 } 1575 1576 private View getSeparatorEntryView(int position, View convertView, ViewGroup parent) { 1577 final SeparatorViewEntry entry = (SeparatorViewEntry) getItem(position); 1578 final View result = (convertView != null) ? convertView : 1579 mInflater.inflate(R.layout.contact_detail_separator_entry_view, parent, false); 1580 1581 result.setPadding(entry.isInSubSection() ? mViewEntryDimensions.getWidePaddingLeft() : 1582 mViewEntryDimensions.getPaddingLeft(), 0, 1583 mViewEntryDimensions.getPaddingRight(), 0); 1584 1585 return result; 1586 } 1587 1588 private View getKindTitleEntryView(int position, View convertView, ViewGroup parent) { 1589 final KindTitleViewEntry entry = (KindTitleViewEntry) getItem(position); 1590 final View result; 1591 final KindTitleViewCache viewCache; 1592 1593 if (convertView != null) { 1594 result = convertView; 1595 viewCache = (KindTitleViewCache)result.getTag(); 1596 } else { 1597 result = mInflater.inflate(R.layout.list_separator, parent, false); 1598 viewCache = new KindTitleViewCache(result); 1599 result.setTag(viewCache); 1600 } 1601 1602 viewCache.titleView.setText(entry.getTitle()); 1603 1604 return result; 1605 } 1606 1607 private View getNetworkTitleEntryView(int position, View convertView, ViewGroup parent) { 1608 final NetworkTitleViewEntry entry = (NetworkTitleViewEntry) getItem(position); 1609 final View result; 1610 final NetworkTitleViewCache viewCache; 1611 1612 if (convertView != null) { 1613 result = convertView; 1614 viewCache = (NetworkTitleViewCache) result.getTag(); 1615 } else { 1616 result = mInflater.inflate(R.layout.contact_detail_network_title_entry_view, 1617 parent, false); 1618 viewCache = new NetworkTitleViewCache(result); 1619 result.setTag(viewCache); 1620 } 1621 1622 viewCache.name.setText(entry.getLabel()); 1623 viewCache.icon.setImageDrawable(entry.getIcon()); 1624 1625 return result; 1626 } 1627 1628 private View getAddConnectionEntryView(int position, View convertView, ViewGroup parent) { 1629 final AddConnectionViewEntry entry = (AddConnectionViewEntry) getItem(position); 1630 final View result; 1631 final AddConnectionViewCache viewCache; 1632 1633 if (convertView != null) { 1634 result = convertView; 1635 viewCache = (AddConnectionViewCache) result.getTag(); 1636 } else { 1637 result = mInflater.inflate(R.layout.contact_detail_add_connection_entry_view, 1638 parent, false); 1639 viewCache = new AddConnectionViewCache(result); 1640 result.setTag(viewCache); 1641 } 1642 viewCache.name.setText(entry.getLabel()); 1643 viewCache.icon.setImageDrawable(entry.getIcon()); 1644 viewCache.primaryActionView.setOnClickListener(entry.mOnClickListener); 1645 1646 return result; 1647 } 1648 1649 private View getDetailEntryView(int position, View convertView, ViewGroup parent) { 1650 final DetailViewEntry entry = (DetailViewEntry) getItem(position); 1651 final View v; 1652 final DetailViewCache viewCache; 1653 1654 // Check to see if we can reuse convertView 1655 if (convertView != null) { 1656 v = convertView; 1657 viewCache = (DetailViewCache) v.getTag(); 1658 } else { 1659 // Create a new view if needed 1660 v = mInflater.inflate(R.layout.contact_detail_list_item, parent, false); 1661 1662 // Cache the children 1663 viewCache = new DetailViewCache(v, 1664 mPrimaryActionClickListener, mSecondaryActionClickListener); 1665 v.setTag(viewCache); 1666 } 1667 1668 bindDetailView(position, v, entry); 1669 return v; 1670 } 1671 1672 private void bindDetailView(int position, View view, DetailViewEntry entry) { 1673 final Resources resources = mContext.getResources(); 1674 DetailViewCache views = (DetailViewCache) view.getTag(); 1675 1676 if (!TextUtils.isEmpty(entry.typeString)) { 1677 views.type.setText(entry.typeString.toUpperCase()); 1678 views.type.setVisibility(View.VISIBLE); 1679 } else { 1680 views.type.setVisibility(View.GONE); 1681 } 1682 1683 views.data.setText(entry.data); 1684 setMaxLines(views.data, entry.maxLines); 1685 1686 // Gray out the data item if it does not perform an action when clicked 1687 // Set primary_text_color even if it might have been set by default to avoid 1688 // views being gray sometimes when they are not supposed to, due to view reuse 1689 ((TextView) view.findViewById(R.id.data)).setTextColor( 1690 getResources().getColor((entry.intent == null) ? 1691 R.color.secondary_text_color : R.color.primary_text_color)); 1692 1693 // Set the default contact method 1694 views.primaryIndicator.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE); 1695 1696 // Set the presence icon 1697 final Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon( 1698 mContext, entry.presence); 1699 final ImageView presenceIconView = views.presenceIcon; 1700 if (presenceIcon != null) { 1701 presenceIconView.setImageDrawable(presenceIcon); 1702 presenceIconView.setVisibility(View.VISIBLE); 1703 } else { 1704 presenceIconView.setVisibility(View.GONE); 1705 } 1706 1707 final ActionsViewContainer actionsButtonContainer = 1708 (ActionsViewContainer) views.actionsViewContainer; 1709 actionsButtonContainer.setTag(entry); 1710 actionsButtonContainer.setPosition(position); 1711 registerForContextMenu(actionsButtonContainer); 1712 1713 // Set the secondary action button 1714 final ImageView secondaryActionView = views.secondaryActionButton; 1715 Drawable secondaryActionIcon = null; 1716 String secondaryActionDescription = null; 1717 if (entry.secondaryActionIcon != -1) { 1718 secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon); 1719 if (ContactDisplayUtils.isCustomPhoneType(entry.type)) { 1720 secondaryActionDescription = resources.getString( 1721 entry.secondaryActionDescription, entry.typeString); 1722 } else { 1723 secondaryActionDescription = resources.getString( 1724 entry.secondaryActionDescription); 1725 } 1726 } else if ((entry.chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { 1727 secondaryActionIcon = 1728 resources.getDrawable(R.drawable.sym_action_videochat_holo_light); 1729 secondaryActionDescription = resources.getString(R.string.video_chat); 1730 } else if ((entry.chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { 1731 secondaryActionIcon = 1732 resources.getDrawable(R.drawable.sym_action_audiochat_holo_light); 1733 secondaryActionDescription = resources.getString(R.string.audio_chat); 1734 } 1735 1736 final View secondaryActionViewContainer = views.secondaryActionViewContainer; 1737 if (entry.secondaryIntent != null && secondaryActionIcon != null) { 1738 secondaryActionView.setImageDrawable(secondaryActionIcon); 1739 secondaryActionView.setContentDescription(secondaryActionDescription); 1740 secondaryActionViewContainer.setTag(entry); 1741 secondaryActionViewContainer.setVisibility(View.VISIBLE); 1742 views.secondaryActionDivider.setVisibility(View.VISIBLE); 1743 } else { 1744 secondaryActionViewContainer.setVisibility(View.GONE); 1745 views.secondaryActionDivider.setVisibility(View.GONE); 1746 } 1747 1748 // Right and left padding should not have "pressed" effect. 1749 view.setPadding( 1750 entry.isInSubSection() 1751 ? mViewEntryDimensions.getWidePaddingLeft() 1752 : mViewEntryDimensions.getPaddingLeft(), 1753 0, mViewEntryDimensions.getPaddingRight(), 0); 1754 // Top and bottom padding should have "pressed" effect. 1755 final View primaryActionView = views.primaryActionView; 1756 primaryActionView.setPadding( 1757 primaryActionView.getPaddingLeft(), 1758 mViewEntryDimensions.getPaddingTop(), 1759 primaryActionView.getPaddingRight(), 1760 mViewEntryDimensions.getPaddingBottom()); 1761 secondaryActionViewContainer.setPadding( 1762 secondaryActionViewContainer.getPaddingLeft(), 1763 mViewEntryDimensions.getPaddingTop(), 1764 secondaryActionViewContainer.getPaddingRight(), 1765 mViewEntryDimensions.getPaddingBottom()); 1766 1767 // Set the text direction 1768 if (entry.textDirection != TEXT_DIRECTION_UNDEFINED) { 1769 views.data.setTextDirection(entry.textDirection); 1770 } 1771 } 1772 1773 private void setMaxLines(TextView textView, int maxLines) { 1774 if (maxLines == 1) { 1775 textView.setSingleLine(true); 1776 textView.setEllipsize(TextUtils.TruncateAt.END); 1777 } else { 1778 textView.setSingleLine(false); 1779 textView.setMaxLines(maxLines); 1780 textView.setEllipsize(null); 1781 } 1782 } 1783 1784 private final OnClickListener mPrimaryActionClickListener = new OnClickListener() { 1785 @Override 1786 public void onClick(View view) { 1787 if (mListener == null) return; 1788 final ViewEntry entry = (ViewEntry) view.getTag(); 1789 if (entry == null) return; 1790 entry.click(view, mListener); 1791 } 1792 }; 1793 1794 private final OnClickListener mSecondaryActionClickListener = new OnClickListener() { 1795 @Override 1796 public void onClick(View view) { 1797 if (mListener == null) return; 1798 if (view == null) return; 1799 final ViewEntry entry = (ViewEntry) view.getTag(); 1800 if (entry == null || !(entry instanceof DetailViewEntry)) return; 1801 final DetailViewEntry detailViewEntry = (DetailViewEntry) entry; 1802 final Intent intent = detailViewEntry.secondaryIntent; 1803 if (intent == null) return; 1804 mListener.onItemClicked(intent); 1805 } 1806 }; 1807 1808 @Override 1809 public int getCount() { 1810 return mAllEntries.size(); 1811 } 1812 1813 @Override 1814 public ViewEntry getItem(int position) { 1815 return mAllEntries.get(position); 1816 } 1817 1818 @Override 1819 public int getItemViewType(int position) { 1820 return mAllEntries.get(position).getViewType(); 1821 } 1822 1823 @Override 1824 public int getViewTypeCount() { 1825 return VIEW_TYPE_COUNT; 1826 } 1827 1828 @Override 1829 public long getItemId(int position) { 1830 final ViewEntry entry = mAllEntries.get(position); 1831 if (entry != null) { 1832 return entry.getId(); 1833 } 1834 return -1; 1835 } 1836 1837 @Override 1838 public boolean areAllItemsEnabled() { 1839 // Header will always be an item that is not enabled. 1840 return false; 1841 } 1842 1843 @Override 1844 public boolean isEnabled(int position) { 1845 return getItem(position).isEnabled(); 1846 } 1847 } 1848 1849 @Override 1850 public void onAccountSelectorCancelled() { 1851 } 1852 1853 @Override 1854 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 1855 createCopy(account); 1856 } 1857 1858 private void createCopy(AccountWithDataSet account) { 1859 if (mListener != null) { 1860 mListener.onCreateRawContactRequested(mContactData.getContentValues(), account); 1861 } 1862 } 1863 1864 /** 1865 * Default (fallback) list item click listener. Note the click event for DetailViewEntry is 1866 * caught by individual views in the list item view to distinguish the primary action and the 1867 * secondary action, so this method won't be invoked for that. (The listener is set in the 1868 * bindview in the adapter) 1869 * This listener is used for other kind of entries. 1870 */ 1871 @Override 1872 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1873 if (mListener == null) return; 1874 final ViewEntry entry = mAdapter.getItem(position); 1875 if (entry == null) return; 1876 entry.click(view, mListener); 1877 } 1878 1879 @Override 1880 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 1881 super.onCreateContextMenu(menu, view, menuInfo); 1882 1883 AdapterView.AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; 1884 DetailViewEntry selectedEntry = (DetailViewEntry) mAllEntries.get(info.position); 1885 1886 menu.setHeaderTitle(selectedEntry.data); 1887 menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT, 1888 ContextMenu.NONE, getString(R.string.copy_text)); 1889 1890 // Don't allow setting or clearing of defaults for directory contacts 1891 if (mContactData.isDirectoryEntry()) { 1892 return; 1893 } 1894 1895 String selectedMimeType = selectedEntry.mimetype; 1896 1897 // Defaults to true will only enable the detail to be copied to the clipboard. 1898 boolean isUniqueMimeType = true; 1899 1900 // Only allow primary support for Phone and Email content types 1901 if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 1902 isUniqueMimeType = mIsUniqueNumber; 1903 } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { 1904 isUniqueMimeType = mIsUniqueEmail; 1905 } 1906 1907 // Checking for previously set default 1908 if (selectedEntry.isPrimary) { 1909 menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT, 1910 ContextMenu.NONE, getString(R.string.clear_default)); 1911 } else if (!isUniqueMimeType) { 1912 menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT, 1913 ContextMenu.NONE, getString(R.string.set_default)); 1914 } 1915 } 1916 1917 @Override 1918 public boolean onContextItemSelected(MenuItem item) { 1919 AdapterView.AdapterContextMenuInfo menuInfo; 1920 try { 1921 menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 1922 } catch (ClassCastException e) { 1923 Log.e(TAG, "bad menuInfo", e); 1924 return false; 1925 } 1926 1927 switch (item.getItemId()) { 1928 case ContextMenuIds.COPY_TEXT: 1929 copyToClipboard(menuInfo.position); 1930 return true; 1931 case ContextMenuIds.SET_DEFAULT: 1932 setDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position)); 1933 return true; 1934 case ContextMenuIds.CLEAR_DEFAULT: 1935 clearDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position)); 1936 return true; 1937 default: 1938 throw new IllegalArgumentException("Unknown menu option " + item.getItemId()); 1939 } 1940 } 1941 1942 private void setDefaultContactMethod(long id) { 1943 Intent setIntent = ContactSaveService.createSetSuperPrimaryIntent(mContext, id); 1944 mContext.startService(setIntent); 1945 } 1946 1947 private void clearDefaultContactMethod(long id) { 1948 Intent clearIntent = ContactSaveService.createClearPrimaryIntent(mContext, id); 1949 mContext.startService(clearIntent); 1950 } 1951 1952 private void copyToClipboard(int viewEntryPosition) { 1953 // Getting the text to copied 1954 DetailViewEntry detailViewEntry = (DetailViewEntry) mAllEntries.get(viewEntryPosition); 1955 CharSequence textToCopy = detailViewEntry.data; 1956 1957 // Checking for empty string 1958 if (TextUtils.isEmpty(textToCopy)) return; 1959 1960 ClipboardUtils.copyText(getActivity(), detailViewEntry.typeString, textToCopy, true); 1961 } 1962 1963 @Override 1964 public boolean handleKeyDown(int keyCode) { 1965 switch (keyCode) { 1966 case KeyEvent.KEYCODE_CALL: { 1967 TelephonyManager telephonyManager = 1968 (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); 1969 if (telephonyManager != null && 1970 telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) { 1971 // Skip out and let the key be handled at a higher level 1972 break; 1973 } 1974 1975 int index = mListView.getSelectedItemPosition(); 1976 if (index != -1) { 1977 final DetailViewEntry entry = (DetailViewEntry) mAdapter.getItem(index); 1978 if (entry != null && entry.intent != null && 1979 entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) { 1980 mContext.startActivity(entry.intent); 1981 return true; 1982 } 1983 } else if (mPrimaryPhoneUri != null) { 1984 // There isn't anything selected, call the default number 1985 mContext.startActivity(CallUtil.getCallIntent(mPrimaryPhoneUri)); 1986 return true; 1987 } 1988 return false; 1989 } 1990 } 1991 1992 return false; 1993 } 1994 1995 /** 1996 * Base class for QuickFixes. QuickFixes quickly fix issues with the Contact without 1997 * requiring the user to go to the editor. Example: Add to My Contacts. 1998 */ 1999 private static abstract class QuickFix { 2000 public abstract boolean isApplicable(); 2001 public abstract String getTitle(); 2002 public abstract void execute(); 2003 } 2004 2005 private class AddToMyContactsQuickFix extends QuickFix { 2006 @Override 2007 public boolean isApplicable() { 2008 // Only local contacts 2009 if (mContactData == null || mContactData.isDirectoryEntry()) return false; 2010 2011 // User profile cannot be added to contacts 2012 if (mContactData.isUserProfile()) return false; 2013 2014 // Only if exactly one raw contact 2015 if (mContactData.getRawContacts().size() != 1) return false; 2016 2017 // test if the default group is assigned 2018 final List<GroupMetaData> groups = mContactData.getGroupMetaData(); 2019 2020 // For accounts without group support, groups is null 2021 if (groups == null) return false; 2022 2023 // remember the default group id. no default group? bail out early 2024 final long defaultGroupId = getDefaultGroupId(groups); 2025 if (defaultGroupId == -1) return false; 2026 2027 final RawContact rawContact = (RawContact) mContactData.getRawContacts().get(0); 2028 final AccountType type = rawContact.getAccountType(getContext()); 2029 // Offline or non-writeable account? Nothing to fix 2030 if (type == null || !type.areContactsWritable()) return false; 2031 2032 // Check whether the contact is in the default group 2033 boolean isInDefaultGroup = false; 2034 for (DataItem dataItem : Iterables.filter( 2035 rawContact.getDataItems(), GroupMembershipDataItem.class)) { 2036 GroupMembershipDataItem groupMembership = (GroupMembershipDataItem) dataItem; 2037 final Long groupId = groupMembership.getGroupRowId(); 2038 if (groupId != null && groupId == defaultGroupId) { 2039 isInDefaultGroup = true; 2040 break; 2041 } 2042 } 2043 2044 return !isInDefaultGroup; 2045 } 2046 2047 @Override 2048 public String getTitle() { 2049 return getString(R.string.add_to_my_contacts); 2050 } 2051 2052 @Override 2053 public void execute() { 2054 final long defaultGroupId = getDefaultGroupId(mContactData.getGroupMetaData()); 2055 // there should always be a default group (otherwise the button would be invisible), 2056 // but let's be safe here 2057 if (defaultGroupId == -1) return; 2058 2059 // add the group membership to the current state 2060 final RawContactDeltaList contactDeltaList = mContactData.createRawContactDeltaList(); 2061 final RawContactDelta rawContactEntityDelta = contactDeltaList.get(0); 2062 2063 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 2064 final AccountType type = rawContactEntityDelta.getAccountType(accountTypes); 2065 final DataKind groupMembershipKind = type.getKindForMimetype( 2066 GroupMembership.CONTENT_ITEM_TYPE); 2067 final ValuesDelta entry = RawContactModifier.insertChild(rawContactEntityDelta, 2068 groupMembershipKind); 2069 if (entry == null) return; 2070 entry.setGroupRowId(defaultGroupId); 2071 2072 // and fire off the intent. we don't need a callback, as the database listener 2073 // should update the ui 2074 final Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), 2075 contactDeltaList, "", 0, false, getActivity().getClass(), 2076 Intent.ACTION_VIEW, null); 2077 getActivity().startService(intent); 2078 } 2079 } 2080 2081 private class MakeLocalCopyQuickFix extends QuickFix { 2082 @Override 2083 public boolean isApplicable() { 2084 // Not a directory contact? Nothing to fix here 2085 if (mContactData == null || !mContactData.isDirectoryEntry()) return false; 2086 2087 // No export support? Too bad 2088 if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) { 2089 return false; 2090 } 2091 2092 return true; 2093 } 2094 2095 @Override 2096 public String getTitle() { 2097 return getString(R.string.menu_copyContact); 2098 } 2099 2100 @Override 2101 public void execute() { 2102 if (mListener == null) { 2103 return; 2104 } 2105 2106 int exportSupport = mContactData.getDirectoryExportSupport(); 2107 switch (exportSupport) { 2108 case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: { 2109 createCopy(new AccountWithDataSet(mContactData.getDirectoryAccountName(), 2110 mContactData.getDirectoryAccountType(), null)); 2111 break; 2112 } 2113 case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: { 2114 final List<AccountWithDataSet> accounts = 2115 AccountTypeManager.getInstance(mContext).getAccounts(true); 2116 if (accounts.isEmpty()) { 2117 createCopy(null); 2118 return; // Don't show a dialog. 2119 } 2120 2121 // In the common case of a single writable account, auto-select 2122 // it without showing a dialog. 2123 if (accounts.size() == 1) { 2124 createCopy(accounts.get(0)); 2125 return; // Don't show a dialog. 2126 } 2127 2128 SelectAccountDialogFragment.show(getFragmentManager(), 2129 ContactDetailFragment.this, R.string.dialog_new_contact_account, 2130 AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, null); 2131 break; 2132 } 2133 } 2134 } 2135 } 2136 2137 /** 2138 * This class loads the correct padding values for a contact detail item so they can be applied 2139 * dynamically. For example, this supports the case where some detail items can be indented and 2140 * need extra padding. 2141 */ 2142 private static class ViewEntryDimensions { 2143 2144 private final int mWidePaddingLeft; 2145 private final int mPaddingLeft; 2146 private final int mPaddingRight; 2147 private final int mPaddingTop; 2148 private final int mPaddingBottom; 2149 2150 public ViewEntryDimensions(Resources resources) { 2151 mPaddingLeft = resources.getDimensionPixelSize( 2152 R.dimen.detail_item_side_margin); 2153 mPaddingTop = resources.getDimensionPixelSize( 2154 R.dimen.detail_item_vertical_margin); 2155 mWidePaddingLeft = mPaddingLeft + 2156 resources.getDimensionPixelSize(R.dimen.detail_item_icon_margin) + 2157 resources.getDimensionPixelSize(R.dimen.detail_network_icon_size); 2158 mPaddingRight = mPaddingLeft; 2159 mPaddingBottom = mPaddingTop; 2160 } 2161 2162 public int getWidePaddingLeft() { 2163 return mWidePaddingLeft; 2164 } 2165 2166 public int getPaddingLeft() { 2167 return mPaddingLeft; 2168 } 2169 2170 public int getPaddingRight() { 2171 return mPaddingRight; 2172 } 2173 2174 public int getPaddingTop() { 2175 return mPaddingTop; 2176 } 2177 2178 public int getPaddingBottom() { 2179 return mPaddingBottom; 2180 } 2181 } 2182 2183 public static interface Listener { 2184 /** 2185 * User clicked a single item (e.g. mail). The intent passed in could be null. 2186 */ 2187 public void onItemClicked(Intent intent); 2188 2189 /** 2190 * User requested creation of a new contact with the specified values. 2191 * 2192 * @param values ContentValues containing data rows for the new contact. 2193 * @param account Account where the new contact should be created. 2194 */ 2195 public void onCreateRawContactRequested(ArrayList<ContentValues> values, 2196 AccountWithDataSet account); 2197 } 2198 2199 /** 2200 * Adapter for the invitable account types; used for the invitable account type list popup. 2201 */ 2202 private final static class InvitableAccountTypesAdapter extends BaseAdapter { 2203 private final Context mContext; 2204 private final LayoutInflater mInflater; 2205 private final ArrayList<AccountType> mAccountTypes; 2206 2207 public InvitableAccountTypesAdapter(Context context, Contact contactData) { 2208 mContext = context; 2209 mInflater = LayoutInflater.from(context); 2210 final List<AccountType> types = contactData.getInvitableAccountTypes(); 2211 mAccountTypes = new ArrayList<AccountType>(types.size()); 2212 2213 for (int i = 0; i < types.size(); i++) { 2214 mAccountTypes.add(types.get(i)); 2215 } 2216 2217 Collections.sort(mAccountTypes, new AccountType.DisplayLabelComparator(mContext)); 2218 } 2219 2220 @Override 2221 public View getView(int position, View convertView, ViewGroup parent) { 2222 final View resultView = 2223 (convertView != null) ? convertView 2224 : mInflater.inflate(R.layout.account_selector_list_item, parent, false); 2225 2226 final TextView text1 = (TextView)resultView.findViewById(android.R.id.text1); 2227 final TextView text2 = (TextView)resultView.findViewById(android.R.id.text2); 2228 final ImageView icon = (ImageView)resultView.findViewById(android.R.id.icon); 2229 2230 final AccountType accountType = mAccountTypes.get(position); 2231 2232 CharSequence action = accountType.getInviteContactActionLabel(mContext); 2233 CharSequence label = accountType.getDisplayLabel(mContext); 2234 if (TextUtils.isEmpty(action)) { 2235 text1.setText(label); 2236 text2.setVisibility(View.GONE); 2237 } else { 2238 text1.setText(action); 2239 text2.setVisibility(View.VISIBLE); 2240 text2.setText(label); 2241 } 2242 icon.setImageDrawable(accountType.getDisplayIcon(mContext)); 2243 2244 return resultView; 2245 } 2246 2247 @Override 2248 public int getCount() { 2249 return mAccountTypes.size(); 2250 } 2251 2252 @Override 2253 public AccountType getItem(int position) { 2254 return mAccountTypes.get(position); 2255 } 2256 2257 @Override 2258 public long getItemId(int position) { 2259 return position; 2260 } 2261 } 2262 } 2263