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