blob: 18c6b6d9765b91f43ec7dd594b9bc4ea5923c72f [file] [log] [blame]
/*
* 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.android.contacts.detail;
import com.android.contacts.Collapser;
import com.android.contacts.Collapser.Collapsible;
import com.android.contacts.ContactLoader;
import com.android.contacts.ContactOptionsActivity;
import com.android.contacts.ContactPresenceIconUtil;
import com.android.contacts.ContactsUtils;
import com.android.contacts.ContactsUtils.ImActions;
import com.android.contacts.GroupMetaData;
import com.android.contacts.R;
import com.android.contacts.TypePrecedence;
import com.android.contacts.editor.SelectAccountDialogFragment;
import com.android.contacts.model.AccountType;
import com.android.contacts.model.AccountType.DataKind;
import com.android.contacts.model.AccountType.EditType;
import com.android.contacts.model.AccountTypes;
import com.android.contacts.util.Constants;
import com.android.contacts.util.DataStatus;
import com.android.contacts.util.DateUtils;
import com.android.contacts.util.PhoneCapabilityTester;
import com.android.contacts.widget.TransitionAnimationView;
import com.android.internal.telephony.ITelephony;
import android.accounts.Account;
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Entity;
import android.content.Entity.NamedContentValues;
import android.content.Intent;
import android.content.Loader;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.ParseException;
import android.net.Uri;
import android.net.WebAddress;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Directory;
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ContactDetailFragment extends Fragment implements OnCreateContextMenuListener,
OnItemClickListener, SelectAccountDialogFragment.Listener {
private static final String TAG = "ContactDetailFragment";
private static final int LOADER_DETAILS = 1;
private static final String KEY_CONTACT_URI = "contactUri";
private static final String LOADER_ARG_CONTACT_URI = "contactUri";
private Context mContext;
private View mView;
private Uri mLookupUri;
private Listener mListener;
private ContactLoader.Result mContactData;
private ContactDetailHeaderView mHeaderView;
private ListView mListView;
private ViewAdapter mAdapter;
private Uri mPrimaryPhoneUri = null;
private Button mCopyGalToLocalButton;
private boolean mAllRestricted;
private final ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
private int mNumPhoneNumbers = 0;
private String mDefaultCountryIso;
private boolean mContactDataDisplayed;
/**
* 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;
/**
* A list of distinct contact IDs included in the current contact.
*/
private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
private ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mRelationEntries = new ArrayList<ViewEntry>();
private ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
private ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
private LayoutInflater mInflater;
private boolean mTransitionAnimationRequested;
public ContactDetailFragment() {
// Explicit constructor for inflation
// Build the list of sections. The order they're added to mSections dictates the
// order they are displayed in the list.
mSections.add(mPhoneEntries);
mSections.add(mSmsEntries);
mSections.add(mEmailEntries);
mSections.add(mImEntries);
mSections.add(mPostalEntries);
mSections.add(mNicknameEntries);
mSections.add(mOtherEntries);
mSections.add(mRelationEntries);
mSections.add(mGroupEntries);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mContext = activity;
mDefaultCountryIso = ContactsUtils.getCurrentCountryIso(mContext);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
mView = inflater.inflate(R.layout.contact_detail_fragment, container, false);
setHasOptionsMenu(true);
mInflater = inflater;
mHeaderView = (ContactDetailHeaderView) mView.findViewById(R.id.contact_header_widget);
mHeaderView.setListener(mHeaderViewListener);
mListView = (ListView) mView.findViewById(android.R.id.list);
mListView.setOnCreateContextMenuListener(this);
mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
mListView.setOnItemClickListener(this);
// Don't set it to mListView yet. We do so later when we bind the adapter.
mEmptyView = mView.findViewById(android.R.id.empty);
mCopyGalToLocalButton = (Button) mView.findViewById(R.id.copyLocal);
mCopyGalToLocalButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
makePersonalCopy();
}
});
mView.setVisibility(View.INVISIBLE);
return mView;
}
public void setListener(Listener value) {
mListener = value;
}
public Uri getUri() {
return mLookupUri;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (mLookupUri != null) {
Bundle args = new Bundle();
args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
getLoaderManager().initLoader(LOADER_DETAILS, args, mDetailLoaderListener);
}
}
public void loadUri(Uri lookupUri) {
if ((lookupUri != null && lookupUri.equals(mLookupUri))
|| (lookupUri == null && mLookupUri == null)) {
return;
}
mLookupUri = lookupUri;
mTransitionAnimationRequested = mContactDataDisplayed;
mContactDataDisplayed = true;
if (mLookupUri == null) {
getLoaderManager().stopLoader(LOADER_DETAILS);
mContactData = null;
bindData();
} else if (getActivity() != null) {
Bundle args = new Bundle();
args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
getLoaderManager().restartLoader(LOADER_DETAILS, args, mDetailLoaderListener);
}
}
private void bindData() {
if (mView == null) {
return;
}
if (isAdded()) {
getActivity().invalidateOptionsMenu();
}
if (mTransitionAnimationRequested) {
TransitionAnimationView.startAnimation(mView, mContactData == null);
mTransitionAnimationRequested = false;
}
if (mContactData == null) {
mView.setVisibility(View.INVISIBLE);
return;
}
// Set the header
mHeaderView.loadData(mContactData);
// Build up the contact entries
buildEntries();
// Collapse similar data items in select sections.
Collapser.collapseList(mPhoneEntries);
Collapser.collapseList(mSmsEntries);
Collapser.collapseList(mEmailEntries);
Collapser.collapseList(mPostalEntries);
Collapser.collapseList(mImEntries);
if (mAdapter == null) {
mAdapter = new ViewAdapter();
mListView.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
mListView.setEmptyView(mEmptyView);
// Configure copy gal button
if (mContactData.isDirectoryEntry()) {
final int exportSupport = mContactData.getDirectoryExportSupport();
if (exportSupport == Directory.EXPORT_SUPPORT_ANY_ACCOUNT
|| exportSupport == Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY) {
mCopyGalToLocalButton.setVisibility(View.VISIBLE);
} else {
mCopyGalToLocalButton.setVisibility(View.GONE);
}
} else {
mCopyGalToLocalButton.setVisibility(View.GONE);
}
mView.setVisibility(View.VISIBLE);
}
/**
* Build up the entries to display on the screen.
*/
private final void buildEntries() {
mHasPhone = PhoneCapabilityTester.isPhone(mContext);
mHasSms = PhoneCapabilityTester.isSmsIntentRegistered(mContext);
mHasSip = PhoneCapabilityTester.isSipPhone(mContext);
// Clear out the old entries
final int numSections = mSections.size();
for (int i = 0; i < numSections; i++) {
mSections.get(i).clear();
}
mRawContactIds.clear();
mAllRestricted = true;
mPrimaryPhoneUri = null;
mNumPhoneNumbers = 0;
mWritableRawContactIds.clear();
final AccountTypes sources = AccountTypes.getInstance(mContext);
// Build up method entries
if (mContactData == null) {
return;
}
ArrayList<String> groups = new ArrayList<String>();
for (Entity entity: mContactData.getEntities()) {
final ContentValues entValues = entity.getEntityValues();
final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
final long rawContactId = entValues.getAsLong(RawContacts._ID);
// Mark when this contact has any unrestricted components
Integer restricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED);
final boolean isRestricted = restricted != null && restricted != 0;
if (!isRestricted) mAllRestricted = false;
if (!mRawContactIds.contains(rawContactId)) {
mRawContactIds.add(rawContactId);
}
AccountType contactsSource = sources.getInflatedSource(accountType,
AccountType.LEVEL_SUMMARY);
if (contactsSource == null || !contactsSource.readOnly) {
mWritableRawContactIds.add(rawContactId);
}
for (NamedContentValues subValue : entity.getSubValues()) {
final ContentValues entryValues = subValue.values;
entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
final long dataId = entryValues.getAsLong(Data._ID);
final String mimeType = entryValues.getAsString(Data.MIMETYPE);
if (mimeType == null) continue;
if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
Long groupId = entryValues.getAsLong(GroupMembership.GROUP_ROW_ID);
if (groupId != null) {
handleGroupMembership(groups, mContactData.getGroupMetaData(), groupId);
}
continue;
}
final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
AccountType.LEVEL_CONSTRAINTS);
if (kind == null) continue;
final ViewEntry entry = ViewEntry.fromValues(mContext, mimeType, kind, dataId,
entryValues);
final boolean hasData = !TextUtils.isEmpty(entry.data);
Integer superPrimary = entryValues.getAsInteger(Data.IS_SUPER_PRIMARY);
final boolean isSuperPrimary = superPrimary != null && superPrimary != 0;
if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
// Always ignore the name. It is shown in the header if set
} else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build phone entries
mNumPhoneNumbers++;
String phoneNumberE164 =
entryValues.getAsString(PhoneLookup.NORMALIZED_NUMBER);
entry.data = PhoneNumberUtils.formatNumber(
entry.data, phoneNumberE164, mDefaultCountryIso);
final Intent phoneIntent = mHasPhone ? new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts(Constants.SCHEME_TEL, entry.data, null)) : null;
final Intent smsIntent = mHasSms ? new Intent(Intent.ACTION_SENDTO,
Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null)) : null;
// Configure Icons and Intents. Notice actionIcon is already set to the phone
if (mHasPhone && mHasSms) {
entry.intent = phoneIntent;
entry.secondaryIntent = smsIntent;
entry.secondaryActionIcon = kind.iconAltRes;
} else if (mHasPhone) {
entry.intent = phoneIntent;
} else if (mHasSms) {
entry.intent = smsIntent;
entry.actionIcon = kind.iconAltRes;
} else {
entry.intent = null;
entry.actionIcon = -1;
}
// Remember super-primary phone
if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
entry.isPrimary = isSuperPrimary;
mPhoneEntries.add(entry);
} else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build email entries
entry.intent = new Intent(Intent.ACTION_SENDTO,
Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
entry.isPrimary = isSuperPrimary;
mEmailEntries.add(entry);
// When Email rows have status, create additional Im row
final DataStatus status = mContactData.getStatuses().get(entry.id);
if (status != null) {
final String imMime = Im.CONTENT_ITEM_TYPE;
final DataKind imKind = sources.getKindOrFallback(accountType,
imMime, mContext, AccountType.LEVEL_MIMETYPES);
final ViewEntry imEntry = ViewEntry.fromValues(mContext,
imMime, imKind, dataId, entryValues);
final ImActions imActions = ContactsUtils.buildImActions(entryValues);
if (imActions != null) {
imEntry.actionIcon = imActions.getPrimaryActionIcon();
imEntry.secondaryActionIcon = imActions.getSecondaryActionIcon();
imEntry.intent = imActions.getPrimaryIntent();
imEntry.secondaryIntent = imActions.getSecondaryIntent();
}
imEntry.applyStatus(status, false);
mImEntries.add(imEntry);
}
} else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build postal entries
entry.maxLines = 4;
entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
mPostalEntries.add(entry);
} else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build IM entries
final ImActions imActions = ContactsUtils.buildImActions(entryValues);
if (imActions != null) {
entry.actionIcon = imActions.getPrimaryActionIcon();
entry.secondaryActionIcon = imActions.getSecondaryActionIcon();
entry.intent = imActions.getPrimaryIntent();
entry.secondaryIntent = imActions.getSecondaryIntent();
}
// Apply presence and status details when available
final DataStatus status = mContactData.getStatuses().get(entry.id);
if (status != null) {
entry.applyStatus(status, false);
}
mImEntries.add(entry);
} else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
// Organizations are not shown. The first one is shown in the header
// and subsequent ones are not supported anymore
} else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && 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 (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build note entries
entry.uri = null;
entry.maxLines = 100;
mOtherEntries.add(entry);
} else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build Website entries
entry.uri = null;
entry.maxLines = 10;
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);
}
mOtherEntries.add(entry);
} else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
// Build SipAddress entries
entry.uri = null;
entry.maxLines = 1;
if (mHasSip) {
entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts(Constants.SCHEME_SIP, entry.data, null));
} else {
entry.intent = null;
entry.actionIcon = -1;
}
mOtherEntries.add(entry);
// TODO: Consider moving the SipAddress into its own
// section (rather than lumping it in with mOtherEntries)
// so that we can reposition 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 (Event.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
entry.data = DateUtils.formatDate(mContext, entry.data);
entry.uri = null;
mOtherEntries.add(entry);
} else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
mRelationEntries.add(entry);
} else {
// Handle showing custom rows
entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
// Use social summary when requested by external source
final DataStatus status = mContactData.getStatuses().get(entry.id);
final boolean hasSocial = kind.actionBodySocial && status != null;
if (hasSocial) {
entry.applyStatus(status, true);
}
if (hasSocial || hasData) {
mOtherEntries.add(entry);
}
}
}
}
if (!groups.isEmpty()) {
ViewEntry entry = new ViewEntry();
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).toUpperCase();
entry.data = sb.toString();
mGroupEntries.add(entry);
}
}
/**
* 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 (!groups.contains(title)) {
groups.add(title);
}
}
break;
}
}
}
private static String buildDataString(DataKind kind, ContentValues values,
Context context) {
if (kind.actionBody == null) {
return null;
}
CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
return actionBody == null ? null : actionBody.toString();
}
/**
* A basic structure with the data for a contact entry in the list.
*/
private static class ViewEntry implements Collapsible<ViewEntry> {
public int type = -1;
public String kind;
public String typeString;
public String data;
public Uri uri;
public long id = 0;
public int maxLines = 1;
public String mimetype;
public Context context = null;
public String resPackageName = null;
public int actionIcon = -1;
public boolean isPrimary = false;
public int secondaryActionIcon = -1;
public Intent intent;
public Intent secondaryIntent = null;
public ArrayList<Long> ids = new ArrayList<Long>();
public int collapseCount = 0;
public int presence = -1;
public CharSequence footerLine = null;
private ViewEntry() {
}
/**
* Build new {@link ViewEntry} and populate from the given values.
*/
public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
long dataId, ContentValues values) {
final ViewEntry entry = new ViewEntry();
entry.context = context;
entry.id = dataId;
entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
entry.mimetype = mimeType;
entry.kind = (kind.titleRes == -1 || kind.titleRes == 0) ? ""
: context.getString(kind.titleRes).toUpperCase();
entry.data = buildDataString(kind, values, context);
if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
entry.type = values.getAsInteger(kind.typeColumn);
// get type string
entry.typeString = "";
for (EditType type : kind.typeList) {
if (type.rawValue == entry.type) {
if (!type.unspecifiedType) {
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 = values.getAsString(type.customColumn);
}
}
break;
}
}
} else {
entry.typeString = "";
}
if (kind.iconRes > 0) {
entry.resPackageName = kind.resPackageName;
entry.actionIcon = kind.iconRes;
}
return entry;
}
/**
* Apply given {@link DataStatus} values over this {@link ViewEntry}
*
* @param fillData When true, the given status replaces {@link #data}
* and {@link #footerLine}. Otherwise only {@link #presence}
* is updated.
*/
public ViewEntry applyStatus(DataStatus status, boolean fillData) {
presence = status.getPresence();
if (fillData && status.isValid()) {
this.data = status.getStatus().toString();
this.footerLine = status.getTimestampLabel(context);
}
return this;
}
@Override
public boolean collapseWith(ViewEntry 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.id);
collapseCount++;
return true;
}
@Override
public boolean shouldCollapseWith(ViewEntry entry) {
if (entry == null) {
return false;
}
if (!ContactsUtils.shouldCollapse(context, mimetype, data, entry.mimetype,
entry.data)) {
return false;
}
if (!TextUtils.equals(mimetype, entry.mimetype)
|| !ContactsUtils.areIntentActionEqual(intent, entry.intent)
|| !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
|| actionIcon != entry.actionIcon) {
return false;
}
return true;
}
}
/** Cache of the children views of a row */
private static class ViewCache {
public View kindDivider;
public View inKindDivider;
public View lineBelowLast;
public TextView kind;
public TextView type;
public TextView data;
public TextView footer;
public ImageView actionIcon;
public ImageView presenceIcon;
public ImageView secondaryActionButton;
public View secondaryActionDivider;
}
private final class ViewAdapter extends BaseAdapter {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewEntry entry = getEntry(position);
final View v;
final ViewCache viewCache;
// Check to see if we can reuse convertView
if (convertView != null) {
v = convertView;
viewCache = (ViewCache) 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 ViewCache();
viewCache.kind = (TextView) v.findViewById(R.id.kind);
viewCache.kindDivider = v.findViewById(R.id.kind_divider);
viewCache.lineBelowLast = v.findViewById(R.id.line_below_last);
viewCache.inKindDivider = v.findViewById(R.id.in_kind_divider);
viewCache.type = (TextView) v.findViewById(R.id.type);
viewCache.data = (TextView) v.findViewById(R.id.data);
viewCache.footer = (TextView) v.findViewById(R.id.footer);
viewCache.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
viewCache.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
viewCache.secondaryActionButton = (ImageView) v.findViewById(
R.id.secondary_action_button);
viewCache.secondaryActionButton.setOnClickListener(mSecondaryActionClickListener);
viewCache.secondaryActionDivider = v.findViewById(R.id.divider);
v.setTag(viewCache);
}
final ViewEntry previousEntry = position == 0 ? null : getEntry(position - 1);
final boolean isFirstOfItsKind =
previousEntry == null ? true : !previousEntry.kind.equals(entry.kind);
final boolean isLast = position == getCount() - 1;
// Bind the data to the view
bindView(v, entry, isFirstOfItsKind, isLast);
return v;
}
protected void bindView(View view, ViewEntry entry, boolean isFirstOfItsKind,
boolean isLast) {
final Resources resources = mContext.getResources();
ViewCache views = (ViewCache) view.getTag();
views.kind.setText(isFirstOfItsKind ? entry.kind : "");
views.kindDivider.setVisibility(isFirstOfItsKind ? View.VISIBLE : View.GONE);
views.inKindDivider.setVisibility(isFirstOfItsKind ? View.GONE : View.VISIBLE);
if (views.lineBelowLast != null) {
views.lineBelowLast.setVisibility(isLast ? View.VISIBLE : View.GONE);
}
views.type.setText(entry.typeString);
views.type.setVisibility(
TextUtils.isEmpty(entry.typeString) ? View.GONE : View.VISIBLE);
// Set the content
final TextView content = views.data;
if (content != null) {
content.setText(entry.data);
setMaxLines(content, entry.maxLines);
}
// Set the footer
if (!TextUtils.isEmpty(entry.footerLine)) {
views.footer.setText(entry.footerLine);
views.footer.setVisibility(View.VISIBLE);
} else {
views.footer.setVisibility(View.GONE);
}
// Set the action icon
final ImageView action = views.actionIcon;
if (entry.actionIcon != -1) {
Drawable actionIcon;
if (entry.resPackageName != null) {
// Load external resources through PackageManager
actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
entry.actionIcon, null);
} else {
actionIcon = resources.getDrawable(entry.actionIcon);
}
action.setImageDrawable(actionIcon);
action.setVisibility(View.VISIBLE);
} else {
// Things should still line up as if there was an icon, so make it invisible
action.setVisibility(View.INVISIBLE);
}
// 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);
}
// Set the secondary action button
final ImageView secondaryActionView = views.secondaryActionButton;
Drawable secondaryActionIcon = null;
if (entry.secondaryActionIcon != -1) {
secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
}
if (entry.secondaryIntent != null && secondaryActionIcon != null) {
secondaryActionView.setImageDrawable(secondaryActionIcon);
secondaryActionView.setTag(entry);
secondaryActionView.setVisibility(View.VISIBLE);
views.secondaryActionDivider.setVisibility(View.VISIBLE);
} else {
secondaryActionView.setVisibility(View.GONE);
views.secondaryActionDivider.setVisibility(View.GONE);
}
}
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 OnClickListener mSecondaryActionClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (mListener == null) return;
if (v == null) return;
final ViewEntry entry = (ViewEntry) v.getTag();
if (entry == null) return;
final Intent intent = entry.secondaryIntent;
if (intent == null) return;
mListener.onItemClicked(intent);
}
};
@Override
public int getCount() {
int count = 0;
final int numSections = mSections.size();
for (int i = 0; i < numSections; i++) {
final ArrayList<ViewEntry> section = mSections.get(i);
count += section.size();
}
return count;
}
@Override
public Object getItem(int position) {
return getEntry(position);
}
@Override
public long getItemId(int position) {
final ViewEntry entry = getEntry(position);
if (entry != null) {
return entry.id;
}
return -1;
}
private ViewEntry getEntry(int position) {
final int numSections = mSections.size();
for (int i = 0; i < numSections; i++) {
final ArrayList<ViewEntry> section = mSections.get(i);
final int sectionSize = section.size();
if (position < sectionSize) {
return section.get(position);
}
position -= sectionSize;
}
return null;
}
}
@Override
public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
inflater.inflate(R.menu.view, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
boolean isEmpty = mContactData == null;
boolean isDirectoryEntry = !isEmpty && mContactData.isDirectoryEntry();
// Options only shows telephony-related settings (ringtone, send to voicemail).
// ==> Hide if we don't have a telephone
final MenuItem optionsMenu = menu.findItem(R.id.menu_options);
final boolean deviceHasPhone = PhoneCapabilityTester.isPhone(mContext);
optionsMenu.setVisible(!isEmpty && !isDirectoryEntry && deviceHasPhone);
final MenuItem editMenu = menu.findItem(R.id.menu_edit);
editMenu.setVisible(!isEmpty && !isDirectoryEntry);
final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
deleteMenu.setVisible(!isEmpty && !isDirectoryEntry);
final MenuItem shareMenu = menu.findItem(R.id.menu_share);
shareMenu.setVisible(!isEmpty && !isDirectoryEntry && !mAllRestricted);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_edit: {
if (mListener != null) mListener.onEditRequested(mLookupUri);
break;
}
case R.id.menu_delete: {
if (mListener != null) mListener.onDeleteRequested(mLookupUri);
return true;
}
case R.id.menu_options: {
if (mContactData == null) return false;
final Intent intent = new Intent(mContext, ContactOptionsActivity.class);
intent.setData(mContactData.getLookupUri());
mContext.startActivity(intent);
return true;
}
case R.id.menu_share: {
if (mAllRestricted) return false;
if (mContactData == null) return false;
final String lookupKey = mContactData.getLookupKey();
final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
final Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(Contacts.CONTENT_VCARD_TYPE);
intent.putExtra(Intent.EXTRA_STREAM, shareUri);
// Launch chooser to share contact via
final CharSequence chooseTitle = mContext.getText(R.string.share_via);
final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
try {
mContext.startActivity(chooseIntent);
} catch (ActivityNotFoundException ex) {
Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
}
return true;
}
}
return false;
}
private void makePersonalCopy() {
if (mListener == null) {
return;
}
int exportSupport = mContactData.getDirectoryExportSupport();
switch (exportSupport) {
case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: {
createCopy(new Account(mContactData.getDirectoryAccountName(),
mContactData.getDirectoryAccountType()));
break;
}
case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: {
final ArrayList<Account> accounts = AccountTypes.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.
}
final SelectAccountDialogFragment dialog = new SelectAccountDialogFragment();
dialog.setTargetFragment(this, 0);
dialog.show(getFragmentManager(), SelectAccountDialogFragment.TAG);
break;
}
}
}
@Override
public void onAccountSelectorCancelled() {
}
@Override
public void onAccountChosen(Account account) {
createCopy(account);
}
private void createCopy(Account account) {
if (mListener != null) {
mListener.onCreateRawContactRequested(mContactData.getContentValues(), account);
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mListener == null) return;
final ViewEntry entry = mAdapter.getEntry(position);
if (entry == null) return;
final Intent intent = entry.intent;
if (intent == null) return;
mListener.onItemClicked(intent);
}
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 ViewEntry entry = mAdapter.getEntry(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
final Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
mPrimaryPhoneUri);
mContext.startActivity(intent);
return true;
}
return false;
}
case KeyEvent.KEYCODE_DEL: {
if (mListener != null) mListener.onDeleteRequested(mLookupUri);
return true;
}
}
return false;
}
/**
* The listener for the detail loader
*/
private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDetailLoaderListener =
new LoaderCallbacks<ContactLoader.Result>() {
@Override
public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */);
}
@Override
public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) {
if (data != ContactLoader.Result.NOT_FOUND && data != ContactLoader.Result.ERROR) {
mContactData = data;
} else {
Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
mContactData = null;
}
bindData();
if (mContactData == null && mListener != null) {
mListener.onContactNotFound();
}
}
};
private ContactDetailHeaderView.Listener mHeaderViewListener =
new ContactDetailHeaderView.Listener() {
@Override
public void onDisplayNameClick(View view) {
}
@Override
public void onPhotoClick(View view) {
}
};
public static interface Listener {
/**
* Contact was not found, so somehow close this fragment. This is raised after a contact
* is removed via Menu/Delete
*/
public void onContactNotFound();
/**
* User decided to go to Edit-Mode
*/
public void onEditRequested(Uri lookupUri);
/**
* User clicked a single item (e.g. mail)
*/
public void onItemClicked(Intent intent);
/**
* User decided to delete the contact
*/
public void onDeleteRequested(Uri lookupUri);
/**
* User requested creation of a new contact with the specified values.
*
* @param values ContentValues containing data rows for the new contact.
* @param account Account where the new contact should be created
*/
public void onCreateRawContactRequested(ArrayList<ContentValues> values, Account account);
}
}