blob: d459d47cebe6872d3dc6d7f735adf7e9a53a9682 [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.list;
import android.content.ContentUris;
import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.net.Uri;
import android.net.Uri.Builder;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Callable;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Directory;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.android.contacts.CallUtil;
import com.android.contacts.ContactPhotoManager.DefaultImageRequest;
import com.android.contacts.ContactsUtils;
import com.android.contacts.GeoUtil;
import com.android.contacts.R;
import com.android.contacts.compat.CallableCompat;
import com.android.contacts.compat.CompatUtils;
import com.android.contacts.compat.DirectoryCompat;
import com.android.contacts.compat.PhoneCompat;
import com.android.contacts.extensions.ExtendedPhoneDirectoriesManager;
import com.android.contacts.extensions.ExtensionsFactory;
import com.android.contacts.preference.ContactsPreferences;
import com.android.contacts.util.Constants;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
/**
* A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and
* {@link SipAddress#CONTENT_ITEM_TYPE}.
*
* By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is
* called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
* API instead of {@link Phone}.
*/
public class PhoneNumberListAdapter extends ContactEntryListAdapter {
private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
public interface Listener {
void onVideoCallIconClicked(int position);
}
// A list of extended directories to add to the directories from the database
private final List<DirectoryPartition> mExtendedDirectories;
// Extended directories will have ID's that are higher than any of the id's from the database,
// so that we can identify them and set them up properly. If no extended directories
// exist, this will be Long.MAX_VALUE
private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
public static class PhoneQuery {
/**
* Optional key used as part of a JSON lookup key to specify an analytics category
* associated with the row.
*/
public static final String ANALYTICS_CATEGORY = "analytics_category";
/**
* Optional key used as part of a JSON lookup key to specify an analytics action associated
* with the row.
*/
public static final String ANALYTICS_ACTION = "analytics_action";
/**
* Optional key used as part of a JSON lookup key to specify an analytics value associated
* with the row.
*/
public static final String ANALYTICS_VALUE = "analytics_value";
public static final String[] PROJECTION_PRIMARY_INTERNAL = new String[] {
Phone._ID, // 0
Phone.TYPE, // 1
Phone.LABEL, // 2
Phone.NUMBER, // 3
Phone.CONTACT_ID, // 4
Phone.LOOKUP_KEY, // 5
Phone.PHOTO_ID, // 6
Phone.DISPLAY_NAME_PRIMARY, // 7
Phone.PHOTO_THUMBNAIL_URI, // 8
};
public static final String[] PROJECTION_PRIMARY;
static {
final List<String> projectionList = Lists.newArrayList(PROJECTION_PRIMARY_INTERNAL);
if (CompatUtils.isMarshmallowCompatible()) {
projectionList.add(Phone.CARRIER_PRESENCE); // 9
}
PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]);
}
public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = new String[] {
Phone._ID, // 0
Phone.TYPE, // 1
Phone.LABEL, // 2
Phone.NUMBER, // 3
Phone.CONTACT_ID, // 4
Phone.LOOKUP_KEY, // 5
Phone.PHOTO_ID, // 6
Phone.DISPLAY_NAME_ALTERNATIVE, // 7
Phone.PHOTO_THUMBNAIL_URI, // 8
};
public static final String[] PROJECTION_ALTERNATIVE;
static {
final List<String> projectionList = Lists.newArrayList(PROJECTION_ALTERNATIVE_INTERNAL);
if (CompatUtils.isMarshmallowCompatible()) {
projectionList.add(Phone.CARRIER_PRESENCE); // 9
}
PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]);
}
public static final int PHONE_ID = 0;
public static final int PHONE_TYPE = 1;
public static final int PHONE_LABEL = 2;
public static final int PHONE_NUMBER = 3;
public static final int CONTACT_ID = 4;
public static final int LOOKUP_KEY = 5;
public static final int PHOTO_ID = 6;
public static final int DISPLAY_NAME = 7;
public static final int PHOTO_URI = 8;
public static final int CARRIER_PRESENCE = 9;
}
private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE =
"length(" + Phone.NUMBER + ") < 1000";
private final CharSequence mUnknownNameText;
private final String mCountryIso;
private ContactListItemView.PhotoPosition mPhotoPosition;
private boolean mUseCallableUri;
private Listener mListener;
private boolean mIsVideoEnabled;
private boolean mIsPresenceEnabled;
public PhoneNumberListAdapter(Context context) {
super(context);
setDefaultFilterHeaderText(R.string.list_filter_phones);
mUnknownNameText = context.getText(android.R.string.unknownName);
mCountryIso = GeoUtil.getCurrentCountryIso(context);
final ExtendedPhoneDirectoriesManager manager
= ExtensionsFactory.getExtendedPhoneDirectoriesManager();
if (manager != null) {
mExtendedDirectories = manager.getExtendedDirectories(mContext);
} else {
// Empty list to avoid sticky NPE's
mExtendedDirectories = new ArrayList<DirectoryPartition>();
}
int videoCapabilities = CallUtil.getVideoCallingAvailability(context);
mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0;
mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
}
protected CharSequence getUnknownNameText() {
return mUnknownNameText;
}
@Override
public void configureLoader(CursorLoader loader, long directoryId) {
String query = getQueryString();
if (query == null) {
query = "";
}
if (isExtendedDirectory(directoryId)) {
final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
final String contentUri = directory.getContentUri();
if (contentUri == null) {
throw new IllegalStateException("Extended directory must have a content URL: "
+ directory);
}
final Builder builder = Uri.parse(contentUri).buildUpon();
builder.appendPath(query);
builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
String.valueOf(getDirectoryResultLimit(directory)));
loader.setUri(builder.build());
loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
} else {
final boolean isRemoteDirectoryQuery
= DirectoryCompat.isRemoteDirectoryId(directoryId);
final Builder builder;
if (isSearchMode()) {
final Uri baseUri;
if (isRemoteDirectoryQuery) {
baseUri = PhoneCompat.getContentFilterUri();
} else if (mUseCallableUri) {
baseUri = CallableCompat.getContentFilterUri();
} else {
baseUri = PhoneCompat.getContentFilterUri();
}
builder = baseUri.buildUpon();
builder.appendPath(query); // Builder will encode the query
builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
String.valueOf(directoryId));
if (isRemoteDirectoryQuery) {
builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
}
} else {
Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
builder = baseUri.buildUpon().appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
if (isSectionHeaderDisplayEnabled()) {
builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true");
}
applyFilter(loader, builder, directoryId, getFilter());
}
// Ignore invalid phone numbers that are too long. These can potentially cause freezes
// in the UI and there is no reason to display them.
final String prevSelection = loader.getSelection();
final String newSelection;
if (!TextUtils.isEmpty(prevSelection)) {
newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE;
} else {
newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE;
}
loader.setSelection(newSelection);
// Remove duplicates when it is possible.
builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
loader.setUri(builder.build());
// TODO a projection that includes the search snippet
if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
} else {
loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
}
if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
} else {
loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
}
}
}
protected boolean isExtendedDirectory(long directoryId) {
return directoryId >= mFirstExtendedDirectoryId;
}
private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
return mExtendedDirectories.get(directoryIndex);
}
/**
* Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
* filter}.
*/
private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId,
ContactListFilter filter) {
if (filter == null || directoryId != Directory.DEFAULT) {
return;
}
final StringBuilder selection = new StringBuilder();
final List<String> selectionArgs = new ArrayList<String>();
switch (filter.filterType) {
case ContactListFilter.FILTER_TYPE_CUSTOM: {
selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
break;
}
case ContactListFilter.FILTER_TYPE_ACCOUNT: {
filter.addAccountQueryParameterToUrl(uriBuilder);
break;
}
case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
case ContactListFilter.FILTER_TYPE_DEFAULT:
break; // No selection needed.
case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
break; // This adapter is always "phone only", so no selection needed either.
default:
Log.w(TAG, "Unsupported filter type came " +
"(type: " + filter.filterType + ", toString: " + filter + ")" +
" showing all contacts.");
// No selection.
break;
}
loader.setSelection(selection.toString());
loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
}
@Override
public String getContactDisplayName(int position) {
return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
}
public String getPhoneNumber(int position) {
final Cursor item = (Cursor)getItem(position);
return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
}
/**
* Builds a {@link Data#CONTENT_URI} for the given cursor position.
*
* @return Uri for the data. may be null if the cursor is not ready.
*/
public Uri getDataUri(int position) {
final int partitionIndex = getPartitionForPosition(position);
final Cursor item = (Cursor)getItem(position);
return item != null ? getDataUri(partitionIndex, item) : null;
}
public Uri getDataUri(int partitionIndex, Cursor cursor) {
final long directoryId =
((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
return null;
} else if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
/*
* ContentUris.withAppendedId(Data.CONTENT_URI, phoneId), is invalid if
* isEnterpriseDirectoryId returns true, because the uri itself will fail since the
* ContactsProvider in Android Framework currently doesn't support it. return null until
* Android framework has enterprise version of Data.CONTENT_URI
*/
return null;
} else {
final long phoneId = cursor.getLong(PhoneQuery.PHONE_ID);
return ContentUris.withAppendedId(Data.CONTENT_URI, phoneId);
}
}
/**
* Retrieves the lookup key for the given cursor position.
*
* @param position The cursor position.
* @return The lookup key.
*/
public String getLookupKey(int position) {
final Cursor item = (Cursor)getItem(position);
return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null;
}
@Override
protected ContactListItemView newView(
Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
ContactListItemView view = super.newView(context, partition, cursor, position, parent);
view.setUnknownNameText(mUnknownNameText);
view.setQuickContactEnabled(isQuickContactEnabled());
view.setPhotoPosition(mPhotoPosition);
return view;
}
protected void setHighlight(ContactListItemView view, Cursor cursor) {
view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
}
// Override default, which would return number of phone numbers, so we
// instead return number of contacts.
@Override
protected int getResultCount(Cursor cursor) {
if (cursor == null) {
return 0;
}
cursor.moveToPosition(-1);
long curContactId = -1;
int numContacts = 0;
while(cursor.moveToNext()) {
final long contactId = cursor.getLong(PhoneQuery.CONTACT_ID);
if (contactId != curContactId) {
curContactId = contactId;
++numContacts;
}
}
return numContacts;
}
@Override
protected void bindView(View itemView, int partition, Cursor cursor, int position) {
super.bindView(itemView, partition, cursor, position);
ContactListItemView view = (ContactListItemView)itemView;
setHighlight(view, cursor);
// Look at elements before and after this position, checking if contact IDs are same.
// If they have one same contact ID, it means they can be grouped.
//
// In one group, only the first entry will show its photo and its name, and the other
// entries in the group show just their data (e.g. phone number, email address).
cursor.moveToPosition(position);
boolean isFirstEntry = true;
boolean showBottomDivider = true;
final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
if (currentContactId == previousContactId) {
isFirstEntry = false;
}
}
cursor.moveToPosition(position);
if (cursor.moveToNext() && !cursor.isAfterLast()) {
final long nextContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
if (currentContactId == nextContactId) {
// The following entry should be in the same group, which means we don't want a
// divider between them.
// TODO: we want a different divider than the divider between groups. Just hiding
// this divider won't be enough.
showBottomDivider = false;
}
}
cursor.moveToPosition(position);
bindViewId(view, cursor, PhoneQuery.PHONE_ID);
bindSectionHeaderAndDivider(view, position);
if (isFirstEntry) {
bindName(view, cursor);
if (isQuickContactEnabled()) {
bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID,
PhoneQuery.LOOKUP_KEY, PhoneQuery.DISPLAY_NAME);
} else {
if (getDisplayPhotos()) {
bindPhoto(view, partition, cursor);
}
}
} else {
unbindName(view);
view.removePhotoView(true, false);
}
final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position);
}
protected void bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber,
int position) {
CharSequence label = null;
if (displayNumber && !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
// TODO cache
label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
}
view.setLabel(label);
final String text;
if (displayNumber) {
text = cursor.getString(PhoneQuery.PHONE_NUMBER);
} else {
// Display phone label. If that's null, display geocoded location for the number
final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
if (phoneLabel != null) {
text = phoneLabel;
} else {
final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
}
}
view.setPhoneNumber(text, mCountryIso);
if (CompatUtils.isVideoCompatible()) {
// Determine if carrier presence indicates the number supports video calling.
int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE);
boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
boolean isVideoIconShown = mIsVideoEnabled && (
mIsPresenceEnabled && isPresent || !mIsPresenceEnabled);
view.setShowVideoCallIcon(isVideoIconShown, mListener, position);
}
}
protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
if (isSectionHeaderDisplayEnabled()) {
Placement placement = getItemPlacementInSection(position);
view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
} else {
view.setSectionHeader(null);
}
}
protected void bindName(final ContactListItemView view, Cursor cursor) {
view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
// Note: we don't show phonetic names any more (see issue 5265330)
}
protected void unbindName(final ContactListItemView view) {
view.hideDisplayName();
}
@Override
protected void bindWorkProfileIcon(final ContactListItemView view, int partition) {
final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
final long directoryId = directory.getDirectoryId();
final long userType = ContactsUtils.determineUserType(directoryId, null);
// Work directory must not be a extended directory. An extended directory is custom
// directory in the app, but not a directory provided by framework. So it can't be
// USER_TYPE_WORK.
view.setWorkProfileIconEnabled(
!isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK);
}
protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
if (!isPhotoSupported(partitionIndex)) {
view.removePhotoView();
return;
}
long photoId = 0;
if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
}
if (photoId != 0) {
getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false,
getCircularPhotos(), null);
} else {
final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
DefaultImageRequest request = null;
if (photoUri == null) {
final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME);
final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY);
request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos());
}
getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
getCircularPhotos(), request);
}
}
public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
mPhotoPosition = photoPosition;
}
public ContactListItemView.PhotoPosition getPhotoPosition() {
return mPhotoPosition;
}
public void setUseCallableUri(boolean useCallableUri) {
mUseCallableUri = useCallableUri;
}
public boolean usesCallableUri() {
return mUseCallableUri;
}
/**
* Override base implementation to inject extended directories between local & remote
* directories. This is done in the following steps:
* 1. Call base implementation to add directories from the cursor.
* 2. Iterate all base directories and establish the following information:
* a. The highest directory id so that we can assign unused id's to the extended directories.
* b. The index of the last non-remote directory. This is where we will insert extended
* directories.
* 3. Iterate the extended directories and for each one, assign an ID and insert it in the
* proper location.
*/
@Override
public void changeDirectories(Cursor cursor) {
super.changeDirectories(cursor);
if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
return;
}
final int numExtendedDirectories = mExtendedDirectories.size();
if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
// already added all directories;
return;
}
//
mFirstExtendedDirectoryId = Long.MAX_VALUE;
if (numExtendedDirectories > 0) {
// The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
// "special" ID.
long maxId = Directory.LOCAL_INVISIBLE;
int insertIndex = 0;
for (int i = 0, n = getPartitionCount(); i < n; i++) {
final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
final long id = partition.getDirectoryId();
if (id > maxId) {
maxId = id;
}
if (!DirectoryCompat.isRemoteDirectoryId(id)) {
// assuming remote directories come after local, we will end up with the index
// where we should insert extended directories. This also works if there are no
// remote directories at all.
insertIndex = i + 1;
}
}
// Extended directories ID's cannot collide with base directories
mFirstExtendedDirectoryId = maxId + 1;
for (int i = 0; i < numExtendedDirectories; i++) {
final long id = mFirstExtendedDirectoryId + i;
final DirectoryPartition directory = mExtendedDirectories.get(i);
if (getPartitionByDirectoryId(id) == -1) {
addPartition(insertIndex, directory);
directory.setDirectoryId(id);
}
}
}
}
@Override
protected Uri getContactUri(int partitionIndex, Cursor cursor,
int contactIdColumn, int lookUpKeyColumn) {
final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex);
final long directoryId = directory.getDirectoryId();
if (!isExtendedDirectory(directoryId)) {
return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn);
}
return Contacts.CONTENT_LOOKUP_URI.buildUpon()
.appendPath(Constants.LOOKUP_URI_ENCODED)
.appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel())
.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
String.valueOf(directoryId))
.encodedFragment(cursor.getString(lookUpKeyColumn))
.build();
}
public Listener getListener() {
return mListener;
}
public void setListener(Listener listener) {
mListener = listener;
}
}