blob: 9a490d78a8a3ac0c57527359b6edb30b3ebc2d3a [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.common.list;
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.Directory;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.R;
import com.android.contacts.common.compat.CallableCompat;
import com.android.contacts.common.compat.DirectoryCompat;
import com.android.contacts.common.compat.PhoneCompat;
import com.android.contacts.common.extensions.PhoneDirectoryExtenderAccessor;
import com.android.contacts.common.list.ContactListItemView.CallToAction;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.Constants;
import com.android.dialer.callcomposer.CallComposerContact;
import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.CompatUtils;
import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.enrichedcall.EnrichedCallManager;
import com.android.dialer.location.GeoUtil;
import com.android.dialer.util.CallUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and {@link
* SipAddress#CONTENT_ITEM_TYPE}.
*
* <p>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();
private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000";
// A list of extended directories to add to the directories from the database
private final List<DirectoryPartition> mExtendedDirectories;
private final CharSequence mUnknownNameText;
private final boolean mCallAndShareEnabled;
// 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;
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);
mExtendedDirectories =
PhoneDirectoryExtenderAccessor.get(mContext).getExtendedDirectories(mContext);
int videoCapabilities = CallUtil.getVideoCallingAvailability(context);
mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0;
mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
// TODO
mCallAndShareEnabled = true;
}
@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:
LogUtil.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]));
}
public String getPhoneNumber(int position) {
final Cursor item = (Cursor) getItem(position);
return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
}
/**
* 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;
}
public CallComposerContact getCallComposerContact(int position) {
Cursor cursor = (Cursor) getItem(position);
if (cursor == null) {
LogUtil.e("PhoneNumberListAdapter.getCallComposerContact", "cursor was null.");
return null;
}
String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME);
String number = cursor.getString(PhoneQuery.PHONE_NUMBER);
String photoUri = cursor.getString(PhoneQuery.PHOTO_URI);
Uri contactUri =
Contacts.getLookupUri(
cursor.getLong(PhoneQuery.CONTACT_ID), cursor.getString(PhoneQuery.LOOKUP_KEY));
CallComposerContact.Builder contact = CallComposerContact.newBuilder();
contact
.setNumber(number)
.setPhotoId(cursor.getLong(PhoneQuery.PHOTO_ID))
.setContactType(ContactPhotoManager.TYPE_DEFAULT)
.setNameOrNumber(displayName)
.setNumberLabel(
Phone.getTypeLabel(
mContext.getResources(),
cursor.getInt(PhoneQuery.PHONE_TYPE),
cursor.getString(PhoneQuery.PHONE_LABEL))
.toString());
if (photoUri != null) {
contact.setPhotoUri(photoUri);
}
if (contactUri != null) {
contact.setContactUri(contactUri.toString());
}
if (!TextUtils.isEmpty(displayName)) {
contact.setDisplayNumber(number);
}
return contact.build();
}
@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
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;
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);
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);
// If the first partition does not have a header, then all subsequent partitions'
// getPositionForPartition returns an index off by 1.
int partitionOffset = 0;
if (partition > 0 && !getPartition(0).getHasHeader()) {
partitionOffset = 1;
}
position += getPositionForPartition(partition) + partitionOffset;
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(mContext.getResources(), type, customLabel);
}
view.setLabel(label);
final String text;
String number = cursor.getString(PhoneQuery.PHONE_NUMBER);
if (displayNumber) {
text = 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);
@CallToAction int action = ContactListItemView.NONE;
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);
if (isVideoIconShown) {
action = ContactListItemView.VIDEO;
}
}
if (isCallAndShareEnabled() && action == ContactListItemView.NONE && number != null) {
EnrichedCallManager manager = EnrichedCallComponent.get(mContext).getEnrichedCallManager();
EnrichedCallCapabilities capabilities = manager.getCapabilities(number);
if (capabilities != null && capabilities.supportsCallComposer()) {
action = ContactListItemView.CALL_AND_SHARE;
} else if (capabilities == null
&& getQueryString() != null
&& getQueryString().length() >= 3) {
manager.requestCapabilities(number);
}
}
view.setCallToAction(action, 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);
// 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 ContactListItemView.PhotoPosition getPhotoPosition() {
return mPhotoPosition;
}
public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
mPhotoPosition = photoPosition;
}
public void setUseCallableUri(boolean useCallableUri) {
mUseCallableUri = useCallableUri;
}
/**
* 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;
}
public boolean isCallAndShareEnabled() {
return mCallAndShareEnabled;
}
public interface Listener {
void onVideoCallIconClicked(int position);
void onCallAndShareIconClicked(int position);
}
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;
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;
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;
static {
final List<String> projectionList =
new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL));
if (CompatUtils.isMarshmallowCompatible()) {
projectionList.add(Phone.CARRIER_PRESENCE); // 9
}
PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]);
}
static {
final List<String> projectionList =
new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL));
if (CompatUtils.isMarshmallowCompatible()) {
projectionList.add(Phone.CARRIER_PRESENCE); // 9
}
PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]);
}
}
}