/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.silentcircle.contacts.detail; import android.app.Activity; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; // import android.net.WebAddress; import android.os.Bundle; import android.os.Parcelable; // import android.os.ServiceManager; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.ListPopupWindow; import android.widget.ListView; import android.widget.TextView; import com.actionbarsherlock.app.SherlockFragment; import com.silentcircle.contacts.ContactPresenceIconUtil; import com.silentcircle.contacts.ContactsUtils; import com.silentcircle.contacts.GroupMetaData; import com.silentcircle.contacts.TypePrecedence; import com.silentcircle.contacts.activities.ScContactDetailActivity; import com.silentcircle.contacts.model.Contact; import com.silentcircle.contacts.model.RawContact; import com.silentcircle.contacts.model.account.AccountType; import com.silentcircle.contacts.model.dataitem.DataItem; import com.silentcircle.contacts.model.dataitem.DataKind; import com.silentcircle.contacts.model.dataitem.EventDataItem; import com.silentcircle.contacts.model.dataitem.NicknameDataItem; import com.silentcircle.contacts.model.dataitem.NoteDataItem; import com.silentcircle.contacts.model.dataitem.PhoneDataItem; import com.silentcircle.contacts.model.dataitem.StructuredNameDataItem; import com.silentcircle.contacts.model.dataitem.StructuredPostalDataItem; import com.silentcircle.contacts.utils.DateUtils; import com.silentcircle.contacts.utils.StructuredPostalUtils; import com.silentcircle.contacts.Collapser; import com.silentcircle.contacts.Collapser.Collapsible; import com.silentcircle.contacts.ScContactSaveService; import com.silentcircle.contacts.R; import com.silentcircle.silentcontacts.ScContactsContract; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Email; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.GroupMembership; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Im; import com.silentcircle.silentcontacts.ScContactsContract.CommonDataKinds.Phone; import com.silentcircle.silentcontacts.ScContactsContract.Data; import com.silentcircle.silentcontacts.ScContactsContract.DisplayNameSources; //import com.android.contacts.model.account.AccountWithDataSet; import com.silentcircle.contacts.model.dataitem.EmailDataItem; import com.silentcircle.contacts.model.dataitem.GroupMembershipDataItem; import com.silentcircle.contacts.model.dataitem.ImDataItem; import com.silentcircle.contacts.model.dataitem.OrganizationDataItem; import com.silentcircle.contacts.model.dataitem.SipAddressDataItem; import com.silentcircle.contacts.utils.Constants; // import com.android.contacts.util.PhoneCapabilityTester; import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public class ContactDetailFragment extends SherlockFragment implements ScContactDetailActivity.FragmentKeyListener, /* SelectAccountDialogFragment.Listener,*/ OnItemClickListener { private static final String TAG = "ContactDetailFragment"; private interface ContextMenuIds { static final int COPY_TEXT = 0; static final int CLEAR_DEFAULT = 1; static final int SET_DEFAULT = 2; } private static final String KEY_CONTACT_URI = "contactUri"; private static final String KEY_LIST_STATE = "liststate"; private Context mContext; private View mView; private OnScrollListener mVerticalScrollListener; private Uri mLookupUri; private Listener mListener; private Contact mContactData; private ViewGroup mStaticPhotoContainer; private View mPhotoTouchOverlay; private ListView mListView; private ViewAdapter mAdapter; private Uri mPrimaryPhoneUri = null; private ViewEntryDimensions mViewEntryDimensions; private final ContactDetailPhotoSetter mPhotoSetter = new ContactDetailPhotoSetter(); private Button mQuickFixButton; // private QuickFix mQuickFix; private String mDefaultCountryIso; private boolean mContactHasSocialUpdates; private boolean mShowStaticPhoto = true; // private final QuickFix[] mPotentialQuickFixes = new QuickFix[] { // new MakeLocalCopyQuickFix(), // new AddToMyContactsQuickFix() // }; /** * Device capability: Set during buildEntries and used in the long-press context menu */ private boolean mHasPhone; /** * Device capability: Set during buildEntries and used in the long-press context menu */ private boolean mHasSms; /** * Device capability: Set during buildEntries and used in the long-press context menu */ private boolean mHasSip; /** * The view shown if the detail list is empty. * We set this to the list view when first bind the adapter, so that it won't be shown while * we're loading data. */ private View mEmptyView; /** * Saved state of the {@link ListView}. This must be saved and applied to the {@ListView} only * when the adapter has been populated again. */ private Parcelable mListState; /** * Lists of specific types of entries to be shown in contact details. */ private ArrayList<DetailViewEntry> mPhoneEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mSmsEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mEmailEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mPostalEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mImEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mNicknameEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mGroupEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mRelationEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mNoteEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mWebsiteEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mSipEntries = new ArrayList<DetailViewEntry>(); private ArrayList<DetailViewEntry> mEventEntries = new ArrayList<DetailViewEntry>(); private final Map<AccountType, List<DetailViewEntry>> mOtherEntriesMap = new HashMap<AccountType, List<DetailViewEntry>>(); private ArrayList<ViewEntry> mAllEntries = new ArrayList<ViewEntry>(); private LayoutInflater mInflater; private boolean mIsUniqueNumber; private boolean mIsUniqueEmail; private ListPopupWindow mPopup; /** * This is to forward touch events to the list view to enable users to scroll the list view * from the blank area underneath the static photo when the layout with static photo is used. */ private OnTouchListener mForwardTouchToListView = new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (mListView != null) { mListView.dispatchTouchEvent(event); return true; } return false; } }; /** * This is to forward drag events to the list view to enable users to scroll the list view * from the blank area underneath the static photo when the layout with static photo is used. */ // private OnDragListener mForwardDragToListView = new OnDragListener() { // @Override // public boolean onDrag(View v, DragEvent event) { // if (mListView != null) { // mListView.dispatchDragEvent(event); // return true; // } // return false; // } // }; public ContactDetailFragment() { // Explicit constructor for inflation } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI); mListState = savedInstanceState.getParcelable(KEY_LIST_STATE); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(KEY_CONTACT_URI, mLookupUri); if (mListView != null) { outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState()); } } @Override public void onPause() { // dismissPopupIfShown(); super.onPause(); } @Override public void onResume() { super.onResume(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mContext = activity; mDefaultCountryIso = ContactsUtils.getCurrentCountryIso(mContext); mViewEntryDimensions = new ViewEntryDimensions(mContext.getResources()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { mView = inflater.inflate(R.layout.contact_detail_fragment, container, false); // Set the touch and drag listener to forward the event to the mListView so that // vertical scrolling can happen from outside of the list view. mView.setOnTouchListener(mForwardTouchToListView); // mView.setOnDragListener(mForwardDragToListView); mInflater = inflater; mStaticPhotoContainer = (ViewGroup) mView.findViewById(R.id.static_photo_container); mPhotoTouchOverlay = mView.findViewById(R.id.photo_touch_intercept_overlay); mListView = (ListView) mView.findViewById(android.R.id.list); mListView.setOnItemClickListener(this); mListView.setItemsCanFocus(true); mListView.setOnScrollListener(mVerticalScrollListener); // Don't set it to mListView yet. We do so later when we bind the adapter. mEmptyView = mView.findViewById(android.R.id.empty); mQuickFixButton = (Button) mView.findViewById(R.id.contact_quick_fix); // mQuickFixButton.setOnClickListener(new OnClickListener() { // @Override // public void onClick(View v) { // if (mQuickFix != null) { // mQuickFix.execute(); // } // } // }); mView.setVisibility(View.INVISIBLE); if (mContactData != null) { bindData(); } return mView; } public void setListener(Listener value) { mListener = value; } protected Context getContext() { return mContext; } protected Listener getListener() { return mListener; } protected Contact getContactData() { return mContactData; } public void setVerticalScrollListener(OnScrollListener listener) { mVerticalScrollListener = listener; } public Uri getUri() { return mLookupUri; } /** * Sets whether the static contact photo (that is not in a scrolling region), should be shown * or not. */ public void setShowStaticPhoto(boolean showPhoto) { mShowStaticPhoto = showPhoto; } /** * Shows the contact detail with a message indicating there are no contact details. */ public void showEmptyState() { setData(null, null); } public void setData(Uri lookupUri, Contact result) { mLookupUri = lookupUri; mContactData = result; bindData(); } /** * Reset the list adapter in this {link Fragment} to get rid of any saved scroll position * from a previous contact. */ public void resetAdapter() { if (mListView != null) { mListView.setAdapter(mAdapter); } } /** * Returns the top coordinate of the first item in the {@link ListView}. If the first item * in the {@link ListView} is not visible or there are no children in the list, then return * Integer.MIN_VALUE. Note that the returned value will be <= 0 because the first item in the * list cannot have a positive offset. */ public int getFirstListItemOffset() { return ContactDetailDisplayUtils.getFirstListItemOffset(mListView); } /** * Tries to scroll the first item to the given offset (this can be a no-op if the list is * already in the correct position). * @param offset which should be <= 0 */ public void requestToMoveToOffset(int offset) { ContactDetailDisplayUtils.requestToMoveToOffset(mListView, offset); } protected void bindData() { if (mView == null) { return; } if (isAdded()) { getActivity().supportInvalidateOptionsMenu(); } if (mContactData == null) { mView.setVisibility(View.INVISIBLE); if (mStaticPhotoContainer != null) { mStaticPhotoContainer.setVisibility(View.GONE); } mAllEntries.clear(); if (mAdapter != null) { mAdapter.notifyDataSetChanged(); } return; } // Figure out if the contact has social updates or not mContactHasSocialUpdates = !mContactData.getStreamItems().isEmpty(); // Setup the photo if applicable if (mStaticPhotoContainer != null) { // The presence of a static photo container is not sufficient to determine whether or // not we should show the photo. Check the mShowStaticPhoto flag which can be set by an // outside class depending on screen size, layout, and whether the contact has social // updates or not. if (mShowStaticPhoto) { mStaticPhotoContainer.setVisibility(View.VISIBLE); final ImageView photoView = (ImageView) mStaticPhotoContainer.findViewById(R.id.photo); final boolean expandPhotoOnClick = mContactData.getPhotoUri() != null; final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick(mContext, mContactData, photoView, expandPhotoOnClick); if (mPhotoTouchOverlay != null) { mPhotoTouchOverlay.setVisibility(View.VISIBLE); if (expandPhotoOnClick || mContactData.isWritableContact(mContext)) { mPhotoTouchOverlay.setOnClickListener(listener); } else { mPhotoTouchOverlay.setClickable(false); } } } else { mStaticPhotoContainer.setVisibility(View.GONE); } } // Build up the contact entries buildEntries(); // Collapse similar data items for select {@link DataKind}s. Collapser.collapseList(mPhoneEntries); Collapser.collapseList(mSmsEntries); Collapser.collapseList(mEmailEntries); Collapser.collapseList(mPostalEntries); Collapser.collapseList(mImEntries); Collapser.collapseList(mEventEntries); mIsUniqueNumber = mPhoneEntries.size() == 1; mIsUniqueEmail = mEmailEntries.size() == 1; // Make one aggregated list of all entries for display to the user. setupFlattenedList(); if (mAdapter == null) { mAdapter = new ViewAdapter(); mListView.setAdapter(mAdapter); } // Restore {@link ListView} state if applicable because the adapter is now populated. if (mListState != null) { mListView.onRestoreInstanceState(mListState); mListState = null; } mAdapter.notifyDataSetChanged(); mListView.setEmptyView(mEmptyView); configureQuickFix(); mView.setVisibility(View.VISIBLE); } /* * Sets {@link #mQuickFix} to a useful action and configures the visibility of * {@link #mQuickFixButton} */ private void configureQuickFix() { // mQuickFix = null; // for (QuickFix fix : mPotentialQuickFixes) { // if (fix.isApplicable()) { // mQuickFix = fix; // break; // } // } // // Configure the button // if (mQuickFix == null) { // mQuickFixButton.setVisibility(View.GONE); // } else { // mQuickFixButton.setVisibility(View.VISIBLE); // mQuickFixButton.setText(mQuickFix.getTitle()); // } } /** @return default group id or -1 if no group or several groups are marked as default */ private long getDefaultGroupId(List<GroupMetaData> groups) { long defaultGroupId = -1; for (GroupMetaData group : groups) { if (group.isDefaultGroup()) { // two default groups? return neither if (defaultGroupId != -1) return -1; defaultGroupId = group.getGroupId(); } } return defaultGroupId; } /** * Build up the entries to display on the screen. */ private final void buildEntries() { mHasPhone = true; // PhoneCapabilityTester.isPhone(mContext); mHasSms = false; // PhoneCapabilityTester.isSmsIntentRegistered(mContext); mHasSip = true; // PhoneCapabilityTester.isSipPhone(mContext); // Clear out the old entries mAllEntries.clear(); mPrimaryPhoneUri = null; // Build up method entries if (mContactData == null) { return; } ArrayList<String> groups = new ArrayList<String>(); for (RawContact rawContact: mContactData.getRawContacts()) { final long rawContactId = rawContact.getId(); for (DataItem dataItem : rawContact.getDataItems()) { dataItem.setRawContactId(rawContactId); if (dataItem.getMimeType() == null) continue; if (dataItem instanceof GroupMembershipDataItem) { GroupMembershipDataItem groupMembership = (GroupMembershipDataItem) dataItem; Long groupId = groupMembership.getGroupRowId(); if (groupId != null) { handleGroupMembership(groups, mContactData.getGroupMetaData(), groupId); } continue; } final DataKind kind = dataItem.getDataKind(); if (kind == null) continue; final DetailViewEntry entry = DetailViewEntry.fromValues(mContext, dataItem, mContactData.isDirectoryEntry(), mContactData.getDirectoryId()); entry.maxLines = kind.maxLinesForDisplay; final boolean hasData = !TextUtils.isEmpty(entry.data); final boolean isSuperPrimary = dataItem.isSuperPrimary(); if (dataItem instanceof StructuredNameDataItem) { // Always ignore the name. It is shown in the header if set } else if (dataItem instanceof PhoneDataItem && hasData) { PhoneDataItem phone = (PhoneDataItem) dataItem; entry.data = phone.getFormattedPhoneNumber(); // Build phone entries final Intent phoneIntent = mHasPhone ? ContactsUtils.getCallIntent(entry.data) : null; final Intent smsIntent = mHasSms ? new Intent(Intent.ACTION_SENDTO, Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null)) : null; // Configure Icons and Intents. if (mHasPhone && mHasSms) { entry.intent = phoneIntent; entry.secondaryIntent = smsIntent; entry.secondaryActionIcon = kind.iconAltRes; entry.secondaryActionDescription = kind.iconAltDescriptionRes; } else if (mHasPhone) { entry.intent = phoneIntent; } else if (mHasSms) { entry.intent = smsIntent; } else { entry.intent = null; } // Remember super-primary phone if (isSuperPrimary) mPrimaryPhoneUri = entry.uri; entry.isPrimary = isSuperPrimary; // If the entry is a primary entry, then render it first in the view. if (entry.isPrimary) { // add to beginning of list so that this phone number shows up first mPhoneEntries.add(0, entry); } else { // add to end of list mPhoneEntries.add(entry); } } else if (dataItem instanceof EmailDataItem && hasData) { // Build email entries entry.intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null)); entry.isPrimary = isSuperPrimary; // If entry is a primary entry, then render it first in the view. if (entry.isPrimary) { mEmailEntries.add(0, entry); } else { mEmailEntries.add(entry); } } else if (dataItem instanceof StructuredPostalDataItem && hasData) { // Build postal entries entry.intent = StructuredPostalUtils.getViewPostalAddressIntent(entry.data); mPostalEntries.add(entry); } else if (dataItem instanceof ImDataItem && hasData) { // Build IM entries buildImActions(mContext, entry, (ImDataItem) dataItem); mImEntries.add(entry); } else if (dataItem instanceof OrganizationDataItem) { // Organizations are not shown. The first one is shown in the header // and subsequent ones are not supported anymore } else if (dataItem instanceof NicknameDataItem && hasData) { // Build nickname entries final boolean isNameRawContact = (mContactData.getNameRawContactId() == rawContactId); final boolean duplicatesTitle = isNameRawContact && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME; if (!duplicatesTitle) { entry.uri = null; mNicknameEntries.add(entry); } } else if (dataItem instanceof NoteDataItem && hasData) { // Build note entries entry.uri = null; mNoteEntries.add(entry); // } else if (dataItem instanceof WebsiteDataItem && hasData) { // // Build Website entries // entry.uri = null; // try { // WebAddress webAddress = new WebAddress(entry.data); // entry.intent = new Intent(Intent.ACTION_VIEW, // Uri.parse(webAddress.toString())); // } catch (ParseException e) { // Log.e(TAG, "Couldn't parse website: " + entry.data); // } // mWebsiteEntries.add(entry); } else if (dataItem instanceof SipAddressDataItem && hasData) { // Build SipAddress entries entry.uri = null; if (mHasSip) { entry.intent = ContactsUtils.getCallIntent( Uri.fromParts(Constants.SCHEME_SIP, entry.data, null)); } else { entry.intent = null; } mSipEntries.add(entry); // TODO: Now that SipAddress is in its own list of entries // (instead of grouped in mOtherEntries), consider // repositioning it right under the phone number. // (Then, we'd also update FallbackAccountType.java to set // secondary=false for this field, and tweak the weight // of its DataKind.) } else if (dataItem instanceof EventDataItem && hasData) { entry.data = DateUtils.formatDate(mContext, entry.data); entry.uri = null; mEventEntries.add(entry); // } else if (dataItem instanceof RelationDataItem && hasData) { // entry.intent = new Intent(Intent.ACTION_SEARCH); // entry.intent.putExtra(SearchManager.QUERY, entry.data); // entry.intent.setType(Contacts.CONTENT_TYPE); // mRelationEntries.add(entry); } else { // Handle showing custom rows // Log.d(TAG, "**** Show custom data rows, dataItem: " + dataItem +", data: " + dataItem.buildDataString()); // entry.intent = new Intent(Intent.ACTION_VIEW); // entry.intent.setDataAndType(entry.uri, entry.mimetype); // // entry.data = dataItem.buildDataString(); // // if (!TextUtils.isEmpty(entry.data)) { // // If the account type exists in the hash map, add it as another entry for // // that account type // AccountType type = dataItem.getAccountType(); // if (mOtherEntriesMap.containsKey(type)) { // List<DetailViewEntry> listEntries = mOtherEntriesMap.get(type); // listEntries.add(entry); // } else { // // Otherwise create a new list with the entry and add it to the hash map // List<DetailViewEntry> listEntries = new ArrayList<DetailViewEntry>(); // listEntries.add(entry); // mOtherEntriesMap.put(type, listEntries); // } // } } } } if (!groups.isEmpty()) { DetailViewEntry entry = new DetailViewEntry(); Collections.sort(groups); StringBuilder sb = new StringBuilder(); int size = groups.size(); for (int i = 0; i < size; i++) { if (i != 0) { sb.append(", "); } sb.append(groups.get(i)); } entry.mimetype = GroupMembership.MIMETYPE; entry.kind = mContext.getString(R.string.groupsLabel); entry.data = sb.toString(); mGroupEntries.add(entry); } } /** * Collapse all contact detail entries into one aggregated list with a {@link HeaderViewEntry} * at the top. */ private void setupFlattenedList() { // All contacts should have a header view (even if there is no data for the contact). mAllEntries.add(new HeaderViewEntry()); addPhoneticName(); flattenList(mPhoneEntries); flattenList(mSmsEntries); flattenList(mSipEntries); flattenList(mImEntries); flattenList(mEmailEntries); flattenList(mNicknameEntries); flattenList(mWebsiteEntries); // addNetworks(); flattenList(mPostalEntries); flattenList(mEventEntries); flattenList(mGroupEntries); flattenList(mRelationEntries); flattenList(mNoteEntries); } /** * Add phonetic name (if applicable) to the aggregated list of contact details. This has to be * done manually because phonetic name doesn't have a mimetype or action intent. */ private void addPhoneticName() { String phoneticName = ContactDetailDisplayUtils.getPhoneticName(mContext, mContactData); if (TextUtils.isEmpty(phoneticName)) { return; } // Add a title String phoneticNameKindTitle = mContext.getString(R.string.name_phonetic); mAllEntries.add(new KindTitleViewEntry(phoneticNameKindTitle.toUpperCase())); // Add the phonetic name final DetailViewEntry entry = new DetailViewEntry(); entry.kind = phoneticNameKindTitle; entry.data = phoneticName; mAllEntries.add(entry); } /** * Add attribution and other third-party entries (if applicable) under the "networks" section * of the aggregated list of contact details. This has to be done manually because the * attribution does not have a mimetype and the third-party entries don't have actually belong * to the same {@link DataKind}. */ // private void addNetworks() { // String attribution = ContactDetailDisplayUtils.getAttribution(mContext, mContactData); // boolean hasAttribution = !TextUtils.isEmpty(attribution); // int networksCount = mOtherEntriesMap.keySet().size(); // // // Note: invitableCount will always be 0 for me profile. (ContactLoader won't set // // invitable types for me profile.) // int invitableCount = mContactData.getInvitableAccountTypes().size(); // if (!hasAttribution && networksCount == 0 && invitableCount == 0) { // return; // } // // // Add a title // String networkKindTitle = mContext.getString(R.string.connections); // mAllEntries.add(new KindTitleViewEntry(networkKindTitle.toUpperCase())); // // // Add the attribution if applicable // if (hasAttribution) { // final DetailViewEntry entry = new DetailViewEntry(); // entry.kind = networkKindTitle; // entry.data = attribution; // mAllEntries.add(entry); // // // Add a divider below the attribution if there are network details that will follow // if (networksCount > 0) { // mAllEntries.add(new SeparatorViewEntry()); // } // } // // // Add the other entries from third parties // for (AccountType accountType : mOtherEntriesMap.keySet()) { // // // Add a title for each third party app // mAllEntries.add(new NetworkTitleViewEntry(mContext, accountType)); // // for (DetailViewEntry detailEntry : mOtherEntriesMap.get(accountType)) { // // Add indented separator // SeparatorViewEntry separatorEntry = new SeparatorViewEntry(); // separatorEntry.setIsInSubSection(true); // mAllEntries.add(separatorEntry); // // // Add indented detail // detailEntry.setIsInSubSection(true); // mAllEntries.add(detailEntry); // } // } // // mOtherEntriesMap.clear(); // // // Add the "More networks" button, which opens the invitable account type list popup. // if (invitableCount > 0) { // addMoreNetworks(); // } // } // // /** // * Add the "More networks" entry. When clicked, show a popup containing a list of invitable // * account types. // */ // private void addMoreNetworks() { // // First, prepare for the popup. // // // Adapter for the list popup. // final InvitableAccountTypesAdapter popupAdapter = new InvitableAccountTypesAdapter(mContext, // mContactData); // // // Listener called when a popup item is clicked. // final AdapterView.OnItemClickListener popupItemListener // = new AdapterView.OnItemClickListener() { // @Override // public void onItemClick(AdapterView<?> parent, View view, int position, // long id) { // if (mListener != null && mContactData != null) { // mListener.onItemClicked(ContactsUtils.getInvitableIntent( // popupAdapter.getItem(position) /* account type */, // mContactData.getLookupUri())); // } // } // }; // // // Then create the click listener for the "More network" entry. Open the popup. // View.OnClickListener onClickListener = new OnClickListener() { // @Override // public void onClick(View v) { // showListPopup(v, popupAdapter, popupItemListener); // } // }; // // // Finally create the entry. // mAllEntries.add(new AddConnectionViewEntry(mContext, onClickListener)); // } /** * Iterate through {@link DetailViewEntry} in the given list and add it to a list of all * entries. Add a {@link KindTitleViewEntry} at the start if the length of the list is not 0. * Add {@link SeparatorViewEntry}s as dividers as appropriate. Clear the original list. */ private void flattenList(ArrayList<DetailViewEntry> entries) { int count = entries.size(); // Add a title for this kind by extracting the kind from the first entry if (count > 0) { String kind = entries.get(0).kind; mAllEntries.add(new KindTitleViewEntry(kind.toUpperCase())); } // Add all the data entries for this kind for (int i = 0; i < count; i++) { // For all entries except the first one, add a divider above the entry if (i != 0) { mAllEntries.add(new SeparatorViewEntry()); } mAllEntries.add(entries.get(i)); } // Clear old list because it's not needed anymore. entries.clear(); } /** * Maps group ID to the corresponding group name, collapses all synonymous groups. * Ignores default groups (e.g. My Contacts) and favorites groups. */ private void handleGroupMembership( ArrayList<String> groups, List<GroupMetaData> groupMetaData, long groupId) { if (groupMetaData == null) { return; } for (GroupMetaData group : groupMetaData) { if (group.getGroupId() == groupId) { if (!group.isDefaultGroup() && !group.isFavorites()) { String title = group.getTitle(); if (!TextUtils.isEmpty(title) && !groups.contains(title)) { groups.add(title); } } break; } } } /** * Writes the Instant Messaging action into the given entry value. */ public static void buildImActions(Context context, DetailViewEntry entry, ImDataItem im) { final boolean isEmail = im.isCreatedFromEmail(); if (!isEmail && !im.isProtocolValid()) { return; } final String data = im.getData(); if (TextUtils.isEmpty(data)) { return; } final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol(); // if (protocol == Im.PROTOCOL_GOOGLE_TALK) { // final int chatCapability = im.getChatCapability(); // entry.chatCapability = chatCapability; // entry.typeString = Im.getProtocolLabel(context.getResources(), Im.PROTOCOL_GOOGLE_TALK, // null).toString(); // if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { // entry.intent = // new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); // entry.secondaryIntent = // new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); // } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { // // Allow Talking and Texting // entry.intent = // new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); // entry.secondaryIntent = // new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call")); // } else { // entry.intent = // new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message")); // } // } else { // Build an IM Intent String host = im.getCustomProtocol(); if (protocol != Im.PROTOCOL_CUSTOM) { // Try bringing in a well-known host for specific protocols host = ContactsUtils.lookupProviderNameFromId(protocol); } if (!TextUtils.isEmpty(host)) { final String authority = host.toLowerCase(); final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority( authority).appendPath(data).build(); entry.intent = new Intent(Intent.ACTION_SENDTO, imUri); // } } } /** * Show a list popup. Used for "popup-able" entry, such as "More networks". */ // private void showListPopup(View anchorView, ListAdapter adapter, // final AdapterView.OnItemClickListener onItemClickListener) { // dismissPopupIfShown(); // mPopup = new ListPopupWindow(mContext, null); // mPopup.setAnchorView(anchorView); // mPopup.setWidth(anchorView.getWidth()); // mPopup.setAdapter(adapter); // mPopup.setModal(true); // // // We need to wrap the passed onItemClickListener here, so that we can dismiss() the // // popup afterwards. Otherwise we could directly use the passed listener. // mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() { // @Override // public void onItemClick(AdapterView<?> parent, View view, int position, // long id) { // onItemClickListener.onItemClick(parent, view, position, id); // dismissPopupIfShown(); // } // }); // mPopup.show(); // } // private void dismissPopupIfShown() { // if (mPopup != null && mPopup.isShowing()) { // mPopup.dismiss(); // } // mPopup = null; // } // /** * Base class for an item in the {@link ViewAdapter} list of data, which is * supplied to the {@link ListView}. */ static class ViewEntry { private final int viewTypeForAdapter; protected long id = -1; /** Whether or not the entry can be focused on or not. */ protected boolean isEnabled = false; ViewEntry(int viewType) { viewTypeForAdapter = viewType; } int getViewType() { return viewTypeForAdapter; } long getId() { return id; } boolean isEnabled(){ return isEnabled; } /** * Called when the entry is clicked. Only {@link #isEnabled} entries can get clicked. * * @param clickedView {@link View} that was clicked (Used, for example, as the anchor view * for a popup.) * @param fragmentListener {@link Listener} set to {@link ContactDetailFragment} */ public void click(View clickedView, Listener fragmentListener) { } } /** * Header item in the {@link ViewAdapter} list of data. */ private static class HeaderViewEntry extends ViewEntry { HeaderViewEntry() { super(ViewAdapter.VIEW_TYPE_HEADER_ENTRY); } } /** * Separator between items of the same {@link DataKind} in the * {@link ViewAdapter} list of data. */ private static class SeparatorViewEntry extends ViewEntry { /** * Whether or not the entry is in a subsection (if true then the contents will be indented * to the right) */ private boolean mIsInSubSection = false; SeparatorViewEntry() { super(ViewAdapter.VIEW_TYPE_SEPARATOR_ENTRY); } public void setIsInSubSection(boolean isInSubSection) { mIsInSubSection = isInSubSection; } public boolean isInSubSection() { return mIsInSubSection; } } /** * Title entry for items of the same {@link DataKind} in the * {@link ViewAdapter} list of data. */ private static class KindTitleViewEntry extends ViewEntry { private final String mTitle; KindTitleViewEntry(String titleText) { super(ViewAdapter.VIEW_TYPE_KIND_TITLE_ENTRY); mTitle = titleText; } public String getTitle() { return mTitle; } } // /** // * A title for a section of contact details from a single 3rd party network. // */ // private static class NetworkTitleViewEntry extends ViewEntry { // private final Drawable mIcon; // private final CharSequence mLabel; // // public NetworkTitleViewEntry(Context context, AccountType type) { // super(ViewAdapter.VIEW_TYPE_NETWORK_TITLE_ENTRY); // this.mIcon = type.getDisplayIcon(context); // this.mLabel = type.getDisplayLabel(context); // this.isEnabled = false; // } // // public Drawable getIcon() { // return mIcon; // } // // public CharSequence getLabel() { // return mLabel; // } // } /** * This is used for the "Add Connections" entry. */ // private static class AddConnectionViewEntry extends ViewEntry { // private final Drawable mIcon; // private final CharSequence mLabel; // private final View.OnClickListener mOnClickListener; // // private AddConnectionViewEntry(Context context, View.OnClickListener onClickListener) { // super(ViewAdapter.VIEW_TYPE_ADD_CONNECTION_ENTRY); // this.mIcon = context.getResources().getDrawable(R.drawable.ic_menu_add_field_holo_light); // this.mLabel = context.getString(R.string.add_connection_button); // this.mOnClickListener = onClickListener; // this.isEnabled = true; // } // // @Override // public void click(View clickedView, Listener fragmentListener) { // if (mOnClickListener == null) return; // mOnClickListener.onClick(clickedView); // } // // public Drawable getIcon() { // return mIcon; // } // // public CharSequence getLabel() { // return mLabel; // } // } /** * An item with a single detail for a contact in the {@link ViewAdapter} * list of data. */ static class DetailViewEntry extends ViewEntry implements Collapsible<DetailViewEntry> { // TODO: Make getters/setters for these fields public int type = -1; public String kind; public String typeString; public String data; public Uri uri; public int maxLines = 1; public String mimetype; public Context context = null; public boolean isPrimary = false; public int secondaryActionIcon = -1; public int secondaryActionDescription = -1; public Intent intent; public Intent secondaryIntent = null; public ArrayList<Long> ids = new ArrayList<Long>(); public int collapseCount = 0; public int presence = -1; public int chatCapability = 0; private boolean mIsInSubSection = false; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("== DetailViewEntry ==\n"); sb.append(" type: " + type + "\n"); sb.append(" kind: " + kind + "\n"); sb.append(" typeString: " + typeString + "\n"); sb.append(" data: " + data + "\n"); sb.append(" uri: " + uri.toString() + "\n"); sb.append(" maxLines: " + maxLines + "\n"); sb.append(" mimetype: " + mimetype + "\n"); sb.append(" isPrimary: " + (isPrimary ? "true" : "false") + "\n"); sb.append(" secondaryActionIcon: " + secondaryActionIcon + "\n"); sb.append(" secondaryActionDescription: " + secondaryActionDescription + "\n"); if (intent == null) { sb.append(" intent: " + intent.toString() + "\n"); } else { sb.append(" intent: " + intent.toString() + "\n"); } if (secondaryIntent == null) { sb.append(" secondaryIntent: (null)\n"); } else { sb.append(" secondaryIntent: " + secondaryIntent.toString() + "\n"); } sb.append(" ids: " + Iterables.toString(ids) + "\n"); sb.append(" collapseCount: " + collapseCount + "\n"); sb.append(" presence: " + presence + "\n"); sb.append(" chatCapability: " + chatCapability + "\n"); sb.append(" mIsInSubsection: " + (mIsInSubSection ? "true" : "false") + "\n"); return sb.toString(); } DetailViewEntry() { super(ViewAdapter.VIEW_TYPE_DETAIL_ENTRY); isEnabled = true; } /** * Build new {@link DetailViewEntry} and populate from the given values. */ public static DetailViewEntry fromValues(Context context, DataItem item, boolean isDirectoryEntry, long directoryId) { final DetailViewEntry entry = new DetailViewEntry(); entry.id = item.getId(); entry.context = context; entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id); if (isDirectoryEntry) { entry.uri = entry.uri.buildUpon().appendQueryParameter( ScContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build(); } entry.mimetype = item.getMimeType(); entry.kind = item.getKindString(); entry.data = item.buildDataString(); if (item.hasKindTypeColumn()) { entry.type = item.getKindTypeColumn(); // get type string entry.typeString = ""; for (AccountType.EditType type : item.getDataKind().typeList) { if (type.rawValue == entry.type) { if (type.customColumn == null) { // Non-custom type. Get its description from the resource entry.typeString = context.getString(type.labelRes); } else { // Custom type. Read it from the database entry.typeString = item.getContentValues().getAsString(type.customColumn); } break; } } } else { entry.typeString = ""; } return entry; } public void setPresence(int presence) { this.presence = presence; } public void setIsInSubSection(boolean isInSubSection) { mIsInSubSection = isInSubSection; } public boolean isInSubSection() { return mIsInSubSection; } @Override public boolean collapseWith(DetailViewEntry entry) { // assert equal collapse keys if (!shouldCollapseWith(entry)) { return false; } // Choose the label associated with the highest type precedence. if (TypePrecedence.getTypePrecedence(mimetype, type) > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) { type = entry.type; kind = entry.kind; typeString = entry.typeString; } // Choose the max of the maxLines and maxLabelLines values. maxLines = Math.max(maxLines, entry.maxLines); // Choose the presence with the highest precedence. // if (StatusUpdates.getPresencePrecedence(presence) // < StatusUpdates.getPresencePrecedence(entry.presence)) { // presence = entry.presence; // } // If any of the collapsed entries are primary make the whole thing primary. isPrimary = entry.isPrimary ? true : isPrimary; // uri, and contactdId, shouldn't make a difference. Just keep the original. // Keep track of all the ids that have been collapsed with this one. ids.add(entry.getId()); collapseCount++; return true; } @Override public boolean shouldCollapseWith(DetailViewEntry entry) { if (entry == null) { return false; } if (!ContactsUtils.shouldCollapse(mimetype, data, entry.mimetype, entry.data)) { return false; } if (!TextUtils.equals(mimetype, entry.mimetype) || !ContactsUtils.areIntentActionEqual(intent, entry.intent) || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)) { return false; } return true; } @Override public void click(View clickedView, Listener fragmentListener) { if (fragmentListener == null || intent == null) return; fragmentListener.onItemClicked(intent); } } /** * Cache of the children views for a view that displays a header view entry. */ private static class HeaderViewCache { public final TextView displayNameView; public final TextView companyView; public final ImageView photoView; public final View photoOverlayView; public final ImageView starredView; public final int layoutResourceId; public HeaderViewCache(View view, int layoutResourceInflated) { displayNameView = (TextView) view.findViewById(R.id.name); companyView = (TextView) view.findViewById(R.id.company); photoView = (ImageView) view.findViewById(R.id.photo); photoOverlayView = view.findViewById(R.id.photo_touch_intercept_overlay); starredView = (ImageView) view.findViewById(R.id.star); layoutResourceId = layoutResourceInflated; } public void enablePhotoOverlay(OnClickListener listener) { if (photoOverlayView != null) { photoOverlayView.setOnClickListener(listener); photoOverlayView.setVisibility(View.VISIBLE); } } } private static class KindTitleViewCache { public final TextView titleView; public KindTitleViewCache(View view) { titleView = (TextView)view.findViewById(R.id.title); } } // /** // * Cache of the children views for a view that displays a {@link NetworkTitleViewEntry} // */ // private static class NetworkTitleViewCache { // public final TextView name; // public final ImageView icon; // // public NetworkTitleViewCache(View view) { // name = (TextView) view.findViewById(R.id.network_title); // icon = (ImageView) view.findViewById(R.id.network_icon); // } // } // /** // * Cache of the children views for a view that displays a {@link AddConnectionViewEntry} // */ // private static class AddConnectionViewCache { // public final TextView name; // public final ImageView icon; // public final View primaryActionView; // // public AddConnectionViewCache(View view) { // name = (TextView) view.findViewById(R.id.add_connection_label); // icon = (ImageView) view.findViewById(R.id.add_connection_icon); // primaryActionView = view.findViewById(R.id.primary_action_view); // } // } /** * Cache of the children views of a contact detail entry represented by a * {@link DetailViewEntry} */ private static class DetailViewCache { public final TextView type; public final TextView data; public final ImageView presenceIcon; public final ImageView secondaryActionButton; public final View actionsViewContainer; public final View primaryActionView; public final View secondaryActionViewContainer; public final View secondaryActionDivider; public final View primaryIndicator; public DetailViewCache(View view, OnClickListener primaryActionClickListener, OnClickListener secondaryActionClickListener) { type = (TextView) view.findViewById(R.id.type); data = (TextView) view.findViewById(R.id.data); primaryIndicator = view.findViewById(R.id.primary_indicator); presenceIcon = (ImageView) view.findViewById(R.id.presence_icon); actionsViewContainer = view.findViewById(R.id.actions_view_container); actionsViewContainer.setOnClickListener(primaryActionClickListener); primaryActionView = view.findViewById(R.id.primary_action_view); secondaryActionViewContainer = view.findViewById(R.id.secondary_action_view_container); secondaryActionViewContainer.setOnClickListener(secondaryActionClickListener); secondaryActionButton = (ImageView) view.findViewById(R.id.secondary_action_button); secondaryActionDivider = view.findViewById(R.id.vertical_divider); } } private final class ViewAdapter extends BaseAdapter { public static final int VIEW_TYPE_DETAIL_ENTRY = 0; public static final int VIEW_TYPE_HEADER_ENTRY = 1; public static final int VIEW_TYPE_KIND_TITLE_ENTRY = 2; public static final int VIEW_TYPE_NETWORK_TITLE_ENTRY = 3; public static final int VIEW_TYPE_ADD_CONNECTION_ENTRY = 4; public static final int VIEW_TYPE_SEPARATOR_ENTRY = 5; private static final int VIEW_TYPE_COUNT = 6; @Override public View getView(int position, View convertView, ViewGroup parent) { switch (getItemViewType(position)) { case VIEW_TYPE_HEADER_ENTRY: return getHeaderEntryView(convertView, parent); case VIEW_TYPE_SEPARATOR_ENTRY: return getSeparatorEntryView(position, convertView, parent); case VIEW_TYPE_KIND_TITLE_ENTRY: return getKindTitleEntryView(position, convertView, parent); case VIEW_TYPE_DETAIL_ENTRY: return getDetailEntryView(position, convertView, parent); // case VIEW_TYPE_NETWORK_TITLE_ENTRY: // return getNetworkTitleEntryView(position, convertView, parent); // case VIEW_TYPE_ADD_CONNECTION_ENTRY: // return getAddConnectionEntryView(position, convertView, parent); default: throw new IllegalStateException("Invalid view type ID " + getItemViewType(position)); } } private View getHeaderEntryView(View convertView, ViewGroup parent) { final int desiredLayoutResourceId = mContactHasSocialUpdates ? R.layout.detail_header_contact_with_updates : R.layout.detail_header_contact_without_updates; View result = null; HeaderViewCache viewCache = null; // Only use convertView if it has the same layout resource ID as the one desired // (the two can be different on wide 2-pane screens where the detail fragment is reused // for many different contacts that do and do not have social updates). if (convertView != null) { viewCache = (HeaderViewCache) convertView.getTag(); if (viewCache.layoutResourceId == desiredLayoutResourceId) { result = convertView; } } // Otherwise inflate a new header view and create a new view cache. if (result == null) { result = mInflater.inflate(desiredLayoutResourceId, parent, false); viewCache = new HeaderViewCache(result, desiredLayoutResourceId); result.setTag(viewCache); } ContactDetailDisplayUtils.setDisplayName(mContext, mContactData, viewCache.displayNameView); ContactDetailDisplayUtils.setCompanyName(mContext, mContactData, viewCache.companyView); // Set the photo if it should be displayed if (viewCache.photoView != null) { final boolean expandOnClick = mContactData.getPhotoUri() != null; final OnClickListener listener = mPhotoSetter.setupContactPhotoForClick( mContext, mContactData, viewCache.photoView, expandOnClick); if (expandOnClick || mContactData.isWritableContact(mContext)) { viewCache.enablePhotoOverlay(listener); } } // Set the starred state if it should be displayed final ImageView favoritesStar = viewCache.starredView; if (favoritesStar != null) { ContactDetailDisplayUtils.configureStarredImageView(favoritesStar, mContactData.isDirectoryEntry(), mContactData.isUserProfile(), mContactData.getStarred()); final Uri lookupUri = mContactData.getLookupUri(); favoritesStar.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Toggle "starred" state // Make sure there is a contact if (lookupUri != null) { // Read the current starred value from the UI instead of using the last // loaded state. This allows rapid tapping without writing the same // value several times final Object tag = favoritesStar.getTag(); final boolean isStarred = tag == null ? false : (Boolean) favoritesStar.getTag(); // To improve responsiveness, swap out the picture (and tag) in the UI // already ContactDetailDisplayUtils.configureStarredImageView(favoritesStar, mContactData.isDirectoryEntry(), mContactData.isUserProfile(), !isStarred); // Now perform the real save Intent intent = ScContactSaveService.createSetStarredIntent(getContext(), lookupUri, !isStarred); getContext().startService(intent); } } }); } return result; } private View getSeparatorEntryView(int position, View convertView, ViewGroup parent) { final SeparatorViewEntry entry = (SeparatorViewEntry) getItem(position); final View result = (convertView != null) ? convertView : mInflater.inflate(R.layout.contact_detail_separator_entry_view, parent, false); result.setPadding(entry.isInSubSection() ? mViewEntryDimensions.getWidePaddingLeft() : mViewEntryDimensions.getPaddingLeft(), 0, mViewEntryDimensions.getPaddingRight(), 0); return result; } private View getKindTitleEntryView(int position, View convertView, ViewGroup parent) { final KindTitleViewEntry entry = (KindTitleViewEntry) getItem(position); final View result; final KindTitleViewCache viewCache; if (convertView != null) { result = convertView; viewCache = (KindTitleViewCache)result.getTag(); } else { result = mInflater.inflate(R.layout.list_separator, parent, false); viewCache = new KindTitleViewCache(result); result.setTag(viewCache); } viewCache.titleView.setText(entry.getTitle()); return result; } // private View getNetworkTitleEntryView(int position, View convertView, ViewGroup parent) { // final NetworkTitleViewEntry entry = (NetworkTitleViewEntry) getItem(position); // final View result; // final NetworkTitleViewCache viewCache; // // if (convertView != null) { // result = convertView; // viewCache = (NetworkTitleViewCache) result.getTag(); // } else { // result = mInflater.inflate(R.layout.contact_detail_network_title_entry_view, // parent, false); // viewCache = new NetworkTitleViewCache(result); // result.setTag(viewCache); // } // // viewCache.name.setText(entry.getLabel()); // viewCache.icon.setImageDrawable(entry.getIcon()); // // return result; // } // private View getAddConnectionEntryView(int position, View convertView, ViewGroup parent) { // final AddConnectionViewEntry entry = (AddConnectionViewEntry) getItem(position); // final View result; // final AddConnectionViewCache viewCache; // // if (convertView != null) { // result = convertView; // viewCache = (AddConnectionViewCache) result.getTag(); // } else { // result = mInflater.inflate(R.layout.contact_detail_add_connection_entry_view, // parent, false); // viewCache = new AddConnectionViewCache(result); // result.setTag(viewCache); // } // viewCache.name.setText(entry.getLabel()); // viewCache.icon.setImageDrawable(entry.getIcon()); // viewCache.primaryActionView.setOnClickListener(entry.mOnClickListener); // // return result; // } private View getDetailEntryView(int position, View convertView, ViewGroup parent) { final DetailViewEntry entry = (DetailViewEntry) getItem(position); final View v; final DetailViewCache viewCache; // Check to see if we can reuse convertView if (convertView != null) { v = convertView; viewCache = (DetailViewCache) v.getTag(); } else { // Create a new view if needed v = mInflater.inflate(R.layout.contact_detail_list_item, parent, false); // Cache the children viewCache = new DetailViewCache(v, mPrimaryActionClickListener, mSecondaryActionClickListener); v.setTag(viewCache); } bindDetailView(position, v, entry); return v; } private void bindDetailView(int position, View view, DetailViewEntry entry) { final Resources resources = mContext.getResources(); DetailViewCache views = (DetailViewCache) view.getTag(); if (!TextUtils.isEmpty(entry.typeString)) { views.type.setText(entry.typeString.toUpperCase()); views.type.setVisibility(View.VISIBLE); } else { views.type.setVisibility(View.GONE); } views.data.setText(entry.data); setMaxLines(views.data, entry.maxLines); // Set the default contact method views.primaryIndicator.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE); // Set the presence icon final Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(mContext, entry.presence); final ImageView presenceIconView = views.presenceIcon; if (presenceIcon != null) { presenceIconView.setImageDrawable(presenceIcon); presenceIconView.setVisibility(View.VISIBLE); } else { presenceIconView.setVisibility(View.GONE); } final ActionsViewContainer actionsButtonContainer = (ActionsViewContainer) views.actionsViewContainer; actionsButtonContainer.setTag(entry); actionsButtonContainer.setPosition(position); registerForContextMenu(actionsButtonContainer); // Set the secondary action button final ImageView secondaryActionView = views.secondaryActionButton; Drawable secondaryActionIcon = null; String secondaryActionDescription = null; if (entry.secondaryActionIcon != -1) { secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon); secondaryActionDescription = resources.getString(entry.secondaryActionDescription); } // else if ((entry.chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) { // secondaryActionIcon = // resources.getDrawable(R.drawable.sym_action_videochat_holo_light); // secondaryActionDescription = resources.getString(R.string.video_chat); // } else if ((entry.chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) { // secondaryActionIcon = // resources.getDrawable(R.drawable.sym_action_audiochat_holo_light); // secondaryActionDescription = resources.getString(R.string.audio_chat); // } final View secondaryActionViewContainer = views.secondaryActionViewContainer; if (entry.secondaryIntent != null && secondaryActionIcon != null) { secondaryActionView.setImageDrawable(secondaryActionIcon); secondaryActionView.setContentDescription(secondaryActionDescription); secondaryActionViewContainer.setTag(entry); secondaryActionViewContainer.setVisibility(View.VISIBLE); views.secondaryActionDivider.setVisibility(View.VISIBLE); } else { secondaryActionViewContainer.setVisibility(View.GONE); views.secondaryActionDivider.setVisibility(View.GONE); } // Right and left padding should not have "pressed" effect. view.setPadding( entry.isInSubSection() ? mViewEntryDimensions.getWidePaddingLeft() : mViewEntryDimensions.getPaddingLeft(), 0, mViewEntryDimensions.getPaddingRight(), 0); // Top and bottom padding should have "pressed" effect. final View primaryActionView = views.primaryActionView; primaryActionView.setPadding( primaryActionView.getPaddingLeft(), mViewEntryDimensions.getPaddingTop(), primaryActionView.getPaddingRight(), mViewEntryDimensions.getPaddingBottom()); secondaryActionViewContainer.setPadding( secondaryActionViewContainer.getPaddingLeft(), mViewEntryDimensions.getPaddingTop(), secondaryActionViewContainer.getPaddingRight(), mViewEntryDimensions.getPaddingBottom()); } private void setMaxLines(TextView textView, int maxLines) { if (maxLines == 1) { textView.setSingleLine(true); textView.setEllipsize(TextUtils.TruncateAt.END); } else { textView.setSingleLine(false); textView.setMaxLines(maxLines); textView.setEllipsize(null); } } private final OnClickListener mPrimaryActionClickListener = new OnClickListener() { @Override public void onClick(View view) { if (mListener == null) return; final ViewEntry entry = (ViewEntry) view.getTag(); if (entry == null) return; entry.click(view, mListener); } }; private final OnClickListener mSecondaryActionClickListener = new OnClickListener() { @Override public void onClick(View view) { if (mListener == null) return; if (view == null) return; final ViewEntry entry = (ViewEntry) view.getTag(); if (entry == null || !(entry instanceof DetailViewEntry)) return; final DetailViewEntry detailViewEntry = (DetailViewEntry)entry; final Intent intent = detailViewEntry.secondaryIntent; if (intent == null) return; mListener.onItemClicked(intent); } }; @Override public int getCount() { return mAllEntries.size(); } @Override public ViewEntry getItem(int position) { return mAllEntries.get(position); } @Override public int getItemViewType(int position) { return mAllEntries.get(position).getViewType(); } @Override public int getViewTypeCount() { return VIEW_TYPE_COUNT; } @Override public long getItemId(int position) { final ViewEntry entry = mAllEntries.get(position); if (entry != null) { return entry.getId(); } return -1; } @Override public boolean areAllItemsEnabled() { // Header will always be an item that is not enabled. return false; } @Override public boolean isEnabled(int position) { return getItem(position).isEnabled(); } } // @Override // public void onAccountSelectorCancelled() { // } // // @Override // public void onAccountChosen(/* AccountWithDataSet account,*/ Bundle extraArgs) { // createCopy(/*account*/); // } private void createCopy(/*AccountWithDataSet account*/) { if (mListener != null) { mListener.onCreateRawContactRequested(mContactData.getContentValues()/*, account*/); } } /** * Default (fallback) list item click listener. Note the click event for DetailViewEntry is * caught by individual views in the list item view to distinguish the primary action and the * secondary action, so this method won't be invoked for that. (The listener is set in the * bindview in the adapter) * This listener is used for other kind of entries. */ @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (mListener == null) return; final ViewEntry entry = mAdapter.getItem(position); if (entry == null) return; entry.click(view, mListener); } @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, view, menuInfo); AdapterView.AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; DetailViewEntry selectedEntry = (DetailViewEntry) mAllEntries.get(info.position); menu.setHeaderTitle(selectedEntry.data); menu.add(ContextMenu.NONE, ContextMenuIds.COPY_TEXT, ContextMenu.NONE, getString(R.string.copy_text)); // Don't allow setting or clearing of defaults for directory contacts if (mContactData.isDirectoryEntry()) { return; } String selectedMimeType = selectedEntry.mimetype; // Defaults to true will only enable the detail to be copied to the clipboard. boolean isUniqueMimeType = true; // Only allow primary support for Phone and Email content types if (Phone.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { isUniqueMimeType = mIsUniqueNumber; } else if (Email.CONTENT_ITEM_TYPE.equals(selectedMimeType)) { isUniqueMimeType = mIsUniqueEmail; } // Checking for previously set default if (selectedEntry.isPrimary) { menu.add(ContextMenu.NONE, ContextMenuIds.CLEAR_DEFAULT, ContextMenu.NONE, getString(R.string.clear_default)); } else if (!isUniqueMimeType) { menu.add(ContextMenu.NONE, ContextMenuIds.SET_DEFAULT, ContextMenu.NONE, getString(R.string.set_default)); } } @Override public boolean onContextItemSelected(MenuItem item) { AdapterView.AdapterContextMenuInfo menuInfo; try { menuInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); } catch (ClassCastException e) { Log.e(TAG, "bad menuInfo", e); return false; } switch (item.getItemId()) { // case ContextMenuIds.COPY_TEXT: // copyToClipboard(menuInfo.position); // return true; case ContextMenuIds.SET_DEFAULT: setDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position)); return true; case ContextMenuIds.CLEAR_DEFAULT: clearDefaultContactMethod(mListView.getItemIdAtPosition(menuInfo.position)); return true; default: throw new IllegalArgumentException("Unknown menu option " + item.getItemId()); } } private void setDefaultContactMethod(long id) { Intent setIntent = ScContactSaveService.createSetSuperPrimaryIntent(mContext, id); mContext.startService(setIntent); } private void clearDefaultContactMethod(long id) { Intent clearIntent = ScContactSaveService.createClearPrimaryIntent(mContext, id); mContext.startService(clearIntent); } // private void copyToClipboard(int viewEntryPosition) { // // Getting the text to copied // DetailViewEntry detailViewEntry = (DetailViewEntry) mAllEntries.get(viewEntryPosition); // CharSequence textToCopy = detailViewEntry.data; // // // Checking for empty string // if (TextUtils.isEmpty(textToCopy)) return; // // ClipboardUtils.copyText(getActivity(), detailViewEntry.typeString, textToCopy, true); // } @Override public boolean handleKeyDown(int keyCode) { // switch (keyCode) { // case KeyEvent.KEYCODE_CALL: { // try { // ITelephony phone = ITelephony.Stub.asInterface( // ServiceManager.checkService("phone")); // if (phone != null && !phone.isIdle()) { // // Skip out and let the key be handled at a higher level // break; // } // } catch (RemoteException re) { // // Fall through and try to call the contact // } // // int index = mListView.getSelectedItemPosition(); // if (index != -1) { // final DetailViewEntry entry = (DetailViewEntry) mAdapter.getItem(index); // if (entry != null && entry.intent != null && // entry.intent.getAction() == Intent.ACTION_CALL_PRIVILEGED) { // mContext.startActivity(entry.intent); // return true; // } // } else if (mPrimaryPhoneUri != null) { // // There isn't anything selected, call the default number // mContext.startActivity(ContactsUtils.getCallIntent(mPrimaryPhoneUri)); // return true; // } // return false; // } // } return false; } // /** // * Base class for QuickFixes. QuickFixes quickly fix issues with the Contact without // * requiring the user to go to the editor. Example: Add to My Contacts. // */ // private static abstract class QuickFix { // public abstract boolean isApplicable(); // public abstract String getTitle(); // public abstract void execute(); // } // // private class AddToMyContactsQuickFix extends QuickFix { // @Override // public boolean isApplicable() { // // Only local contacts // if (mContactData == null || mContactData.isDirectoryEntry()) return false; // // // User profile cannot be added to contacts // if (mContactData.isUserProfile()) return false; // // // Only if exactly one raw contact // if (mContactData.getRawContacts().size() != 1) return false; // // // test if the default group is assigned // final List<GroupMetaData> groups = mContactData.getGroupMetaData(); // // // For accounts without group support, groups is null // if (groups == null) return false; // // // remember the default group id. no default group? bail out early // final long defaultGroupId = getDefaultGroupId(groups); // if (defaultGroupId == -1) return false; // // final RawContact rawContact = (RawContact) mContactData.getRawContacts().get(0); // final AccountType type = rawContact.getAccountType(); // // Offline or non-writeable account? Nothing to fix // if (type == null || !type.areContactsWritable()) return false; // // // Check whether the contact is in the default group // boolean isInDefaultGroup = false; // for (DataItem dataItem : Iterables.filter( // rawContact.getDataItems(), GroupMembershipDataItem.class)) { // GroupMembershipDataItem groupMembership = (GroupMembershipDataItem) dataItem; // final Long groupId = groupMembership.getGroupRowId(); // if (groupId == defaultGroupId) { // isInDefaultGroup = true; // break; // } // } // // return !isInDefaultGroup; // } // // @Override // public String getTitle() { // return getString(R.string.add_to_my_contacts); // } // // @Override // public void execute() { // final long defaultGroupId = getDefaultGroupId(mContactData.getGroupMetaData()); // // there should always be a default group (otherwise the button would be invisible), // // but let's be safe here // if (defaultGroupId == -1) return; // // // add the group membership to the current state // final RawContactDeltaList contactDeltaList = mContactData.createRawContactDeltaList(); // final RawContactDelta rawContactEntityDelta = contactDeltaList.get(0); // // final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); // final AccountType type = rawContactEntityDelta.getAccountType(accountTypes); // final DataKind groupMembershipKind = type.getKindForMimetype( // GroupMembership.CONTENT_ITEM_TYPE); // final ValuesDelta entry = RawContactModifier.insertChild(rawContactEntityDelta, // groupMembershipKind); // entry.setGroupRowId(defaultGroupId); // // // and fire off the intent. we don't need a callback, as the database listener // // should update the ui // final Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), // contactDeltaList, "", 0, false, getActivity().getClass(), // Intent.ACTION_VIEW, null); // getActivity().startService(intent); // } // } // // private class MakeLocalCopyQuickFix extends QuickFix { // @Override // public boolean isApplicable() { // // Not a directory contact? Nothing to fix here // if (mContactData == null || !mContactData.isDirectoryEntry()) return false; // // // No export support? Too bad // if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) { // return false; // } // // return true; // } // // @Override // public String getTitle() { // return getString(R.string.menu_copyContact); // } // // @Override // public void execute() { // if (mListener == null) { // return; // } // // int exportSupport = mContactData.getDirectoryExportSupport(); // switch (exportSupport) { // case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: { // createCopy(new AccountWithDataSet(mContactData.getDirectoryAccountName(), // mContactData.getDirectoryAccountType(), null)); // break; // } // case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: { // final List<AccountWithDataSet> accounts = // AccountTypeManager.getInstance(mContext).getAccounts(true); // if (accounts.isEmpty()) { // createCopy(null); // return; // Don't show a dialog. // } // // // In the common case of a single writable account, auto-select // // it without showing a dialog. // if (accounts.size() == 1) { // createCopy(accounts.get(0)); // return; // Don't show a dialog. // } // // SelectAccountDialogFragment.show(getFragmentManager(), // ContactDetailFragment.this, R.string.dialog_new_contact_account, // AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, null); // break; // } // } // } // } /** * This class loads the correct padding values for a contact detail item so they can be applied * dynamically. For example, this supports the case where some detail items can be indented and * need extra padding. */ private static class ViewEntryDimensions { private final int mWidePaddingLeft; private final int mPaddingLeft; private final int mPaddingRight; private final int mPaddingTop; private final int mPaddingBottom; public ViewEntryDimensions(Resources resources) { mPaddingLeft = resources.getDimensionPixelSize(R.dimen.detail_item_side_margin); mPaddingTop = resources.getDimensionPixelSize(R.dimen.detail_item_vertical_margin); mWidePaddingLeft = mPaddingLeft + resources.getDimensionPixelSize(R.dimen.detail_item_icon_margin) + resources.getDimensionPixelSize(R.dimen.detail_network_icon_size); mPaddingRight = mPaddingLeft; mPaddingBottom = mPaddingTop; } public int getWidePaddingLeft() { return mWidePaddingLeft; } public int getPaddingLeft() { return mPaddingLeft; } public int getPaddingRight() { return mPaddingRight; } public int getPaddingTop() { return mPaddingTop; } public int getPaddingBottom() { return mPaddingBottom; } } public static interface Listener { /** * User clicked a single item (e.g. mail). The intent passed in could be null. */ public void onItemClicked(Intent intent); /** * User requested creation of a new contact with the specified values. * * @param values ContentValues containing data rows for the new contact. */ public void onCreateRawContactRequested(ArrayList<ContentValues> values); } /** * Adapter for the invitable account types; used for the invitable account type list popup. */ // private final static class InvitableAccountTypesAdapter extends BaseAdapter { // private final Context mContext; // private final LayoutInflater mInflater; // private final ArrayList<AccountType> mAccountTypes; // // public InvitableAccountTypesAdapter(Context context, Contact contactData) { // mContext = context; // mInflater = LayoutInflater.from(context); // final List<AccountType> types = contactData.getInvitableAccountTypes(); // mAccountTypes = new ArrayList<AccountType>(types.size()); // // for (int i = 0; i < types.size(); i++) { // mAccountTypes.add(types.get(i)); // } // // Collections.sort(mAccountTypes, new AccountType.DisplayLabelComparator(mContext)); // } // // @Override // public View getView(int position, View convertView, ViewGroup parent) { // final View resultView = // (convertView != null) ? convertView // : mInflater.inflate(R.layout.account_selector_list_item, parent, false); // // final TextView text1 = (TextView)resultView.findViewById(android.R.id.text1); // final TextView text2 = (TextView)resultView.findViewById(android.R.id.text2); // final ImageView icon = (ImageView)resultView.findViewById(android.R.id.icon); // // final AccountType accountType = mAccountTypes.get(position); // // CharSequence action = accountType.getInviteContactActionLabel(mContext); // CharSequence label = accountType.getDisplayLabel(mContext); // if (TextUtils.isEmpty(action)) { // text1.setText(label); // text2.setVisibility(View.GONE); // } else { // text1.setText(action); // text2.setVisibility(View.VISIBLE); // text2.setText(label); // } // icon.setImageDrawable(accountType.getDisplayIcon(mContext)); // // return resultView; // } // // @Override // public int getCount() { // return mAccountTypes.size(); // } // // @Override // public AccountType getItem(int position) { // return mAccountTypes.get(position); // } // // @Override // public long getItemId(int position) { // return position; // } // } }