blob: 56137948b0334d3174772de6f147981adf85f0f3 [file] [log] [blame]
/*
* Copyright (C) 2007 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;
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.Sources;
import com.android.contacts.ui.DisplayGroupsActivity;
import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
import com.android.contacts.util.AccountSelectionUtil;
import com.android.contacts.util.Constants;
import android.accounts.Account;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ListActivity;
import android.app.SearchManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.content.res.Resources;
import android.database.CharArrayBuffer;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.Uri.Builder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.Settings;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.People;
import android.provider.Contacts.PeopleColumns;
import android.provider.Contacts.Phones;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.Presence;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Contacts.AggregationSuggestions;
import android.provider.ContactsContract.Intents.Insert;
import android.provider.ContactsContract.Intents.UI;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
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.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AlphabetIndexer;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.QuickContactBadge;
import android.widget.ResourceCursorAdapter;
import android.widget.SectionIndexer;
import android.widget.TextView;
import android.widget.AbsListView.OnScrollListener;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*TODO(emillar) I commented most of the code that deals with modes and filtering. It should be
* brought back in as we add back that functionality.
*/
/**
* Displays a list of contacts. Usually is embedded into the ContactsActivity.
*/
@SuppressWarnings("deprecation")
public class ContactsListActivity extends ListActivity implements
View.OnCreateContextMenuListener, View.OnClickListener {
public static class JoinContactActivity extends ContactsListActivity {
}
private static final String TAG = "ContactsListActivity";
private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true;
private static final String LIST_STATE_KEY = "liststate";
private static final String FOCUS_KEY = "focused";
static final int MENU_ITEM_VIEW_CONTACT = 1;
static final int MENU_ITEM_CALL = 2;
static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
static final int MENU_ITEM_SEND_SMS = 4;
static final int MENU_ITEM_SEND_IM = 5;
static final int MENU_ITEM_EDIT = 6;
static final int MENU_ITEM_DELETE = 7;
static final int MENU_ITEM_TOGGLE_STAR = 8;
private static final int SUBACTIVITY_NEW_CONTACT = 1;
private static final int SUBACTIVITY_VIEW_CONTACT = 2;
private static final int SUBACTIVITY_DISPLAY_GROUP = 3;
/**
* The action for the join contact activity.
* <p>
* Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID.
*
* TODO: move to {@link ContactsContract}.
*/
public static final String JOIN_AGGREGATE =
"com.android.contacts.action.JOIN_AGGREGATE";
/**
* Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation.
* <p>
* Type: LONG
*/
public static final String EXTRA_AGGREGATE_ID =
"com.android.contacts.action.AGGREGATE_ID";
/**
* Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target.
* <p>
* Type: STRING
*/
@Deprecated
public static final String EXTRA_AGGREGATE_NAME =
"com.android.contacts.action.AGGREGATE_NAME";
public static final String AUTHORITIES_FILTER_KEY = "authorities";
/** Mask for picker mode */
static final int MODE_MASK_PICKER = 0x80000000;
/** Mask for no presence mode */
static final int MODE_MASK_NO_PRESENCE = 0x40000000;
/** Mask for enabling list filtering */
static final int MODE_MASK_NO_FILTER = 0x20000000;
/** Mask for having a "create new contact" header in the list */
static final int MODE_MASK_CREATE_NEW = 0x10000000;
/** Mask for showing photos in the list */
static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
/** Mask for hiding additional information e.g. primary phone number in the list */
static final int MODE_MASK_NO_DATA = 0x04000000;
/** Mask for showing a call button in the list */
static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000;
/** Mask to disable quickcontact (images will show as normal images) */
static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000;
/** Mask to show the total number of contacts at the top */
static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000;
/** Unknown mode */
static final int MODE_UNKNOWN = 0;
/** Default mode */
static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
/** Custom mode */
static final int MODE_CUSTOM = 8;
/** Show all starred contacts */
static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS;
/** Show frequently contacted contacts */
static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS;
/** Show starred and the frequent */
static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON;
/** Show all contacts and pick them when clicking */
static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS
| MODE_MASK_DISABLE_QUIKCCONTACT;
/** Show all contacts as well as the option to create a new one */
static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
| MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
/** Show all people through the legacy provider and pick them when clicking */
static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS
| MODE_MASK_DISABLE_QUIKCCONTACT;
/** Show all people through the legacy provider as well as the option to create a new one */
static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER
| MODE_MASK_CREATE_NEW | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
/** Show all contacts and pick them when clicking, and allow creating a new contact */
static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
/** Show all phone numbers and pick them when clicking */
static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
/** Show all phone numbers through the legacy provider and pick them when clicking */
static final int MODE_LEGACY_PICK_PHONE =
51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
/** Show all postal addresses and pick them when clicking */
static final int MODE_PICK_POSTAL =
55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
/** Show all postal addresses and pick them when clicking */
static final int MODE_LEGACY_PICK_POSTAL =
56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS;
/** Run a search query */
static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
/** Run a search query in PICK mode, but that still launches to VIEW */
static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;
/** Show join suggestions followed by an A-Z list */
static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
| MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
/** Maximum number of suggestions shown for joining aggregates */
static final int MAX_SUGGESTIONS = 4;
static final String NAME_COLUMN = Contacts.DISPLAY_NAME;
//static final String SORT_STRING = People.SORT_STRING;
static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
Contacts._ID, // 0
Contacts.DISPLAY_NAME, // 1
Contacts.STARRED, //2
Contacts.TIMES_CONTACTED, //3
Contacts.CONTACT_PRESENCE, //4
Contacts.PHOTO_ID, //5
Contacts.LOOKUP_KEY, //6
Contacts.HAS_PHONE_NUMBER, //7
};
static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] {
Contacts._ID, // 0
Contacts.DISPLAY_NAME, // 1
Contacts.STARRED, //2
Contacts.TIMES_CONTACTED, //3
Contacts.CONTACT_PRESENCE, //4
Contacts.PHOTO_ID, //5
Contacts.LOOKUP_KEY, //6
// email lookup doesn't included HAS_PHONE_NUMBER OR LOOKUP_KEY in projection
};
static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
People._ID, // 0
People.DISPLAY_NAME, // 1
People.STARRED, //2
PeopleColumns.TIMES_CONTACTED, //3
People.PRESENCE_STATUS, //4
};
static final int SUMMARY_ID_COLUMN_INDEX = 0;
static final int SUMMARY_NAME_COLUMN_INDEX = 1;
static final int SUMMARY_STARRED_COLUMN_INDEX = 2;
static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 3;
static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 4;
static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 5;
static final int SUMMARY_LOOKUP_KEY = 6;
static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 7;
static final String[] PHONES_PROJECTION = new String[] {
Phone._ID, //0
Phone.TYPE, //1
Phone.LABEL, //2
Phone.NUMBER, //3
Phone.DISPLAY_NAME, // 4
Phone.CONTACT_ID, // 5
};
static final String[] LEGACY_PHONES_PROJECTION = new String[] {
Phones._ID, //0
Phones.TYPE, //1
Phones.LABEL, //2
Phones.NUMBER, //3
People.DISPLAY_NAME, // 4
};
static final int PHONE_ID_COLUMN_INDEX = 0;
static final int PHONE_TYPE_COLUMN_INDEX = 1;
static final int PHONE_LABEL_COLUMN_INDEX = 2;
static final int PHONE_NUMBER_COLUMN_INDEX = 3;
static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4;
static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5;
static final String[] POSTALS_PROJECTION = new String[] {
StructuredPostal._ID, //0
StructuredPostal.TYPE, //1
StructuredPostal.LABEL, //2
StructuredPostal.DATA, //3
StructuredPostal.DISPLAY_NAME, // 4
};
static final String[] LEGACY_POSTALS_PROJECTION = new String[] {
ContactMethods._ID, //0
ContactMethods.TYPE, //1
ContactMethods.LABEL, //2
ContactMethods.DATA, //3
People.DISPLAY_NAME, // 4
};
static final String[] RAW_CONTACTS_PROJECTION = new String[] {
RawContacts._ID, //0
RawContacts.CONTACT_ID, //1
RawContacts.ACCOUNT_TYPE, //2
};
static final int POSTAL_ID_COLUMN_INDEX = 0;
static final int POSTAL_TYPE_COLUMN_INDEX = 1;
static final int POSTAL_LABEL_COLUMN_INDEX = 2;
static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
private static final int QUERY_TOKEN = 42;
static final String KEY_PICKER_MODE = "picker_mode";
private ContactItemListAdapter mAdapter;
int mMode = MODE_DEFAULT;
private QueryHandler mQueryHandler;
private boolean mJustCreated;
private boolean mSyncEnabled;
private Uri mSelectedContactUri;
// private boolean mDisplayAll;
private boolean mDisplayOnlyPhones;
private Uri mGroupUri;
private long mQueryAggregateId;
private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
private int mWritableSourcesCnt;
private int mReadOnlySourcesCnt;
/**
* Used to keep track of the scroll state of the list.
*/
private Parcelable mListState = null;
private boolean mListHasFocus;
private String mShortcutAction;
private int mScrollState;
/**
* Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
*/
private int mQueryMode = QUERY_MODE_NONE;
private static final int QUERY_MODE_NONE = -1;
private static final int QUERY_MODE_MAILTO = 1;
private static final int QUERY_MODE_TEL = 2;
/**
* Data to use when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. Usually
* provided by scheme-specific part of incoming {@link Intent#getData()}.
*/
private String mQueryData;
private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
/**
* In the {@link #MODE_JOIN} determines whether we display a list item with the label
* "Show all contacts" or actually show all contacts
*/
private boolean mJoinModeShowAllContacts;
/**
* The ID of the special item described above.
*/
private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2;
// Uri matcher for contact id
private static final int CONTACTS_ID = 1001;
private static final UriMatcher sContactsIdMatcher;
private static ExecutorService sImageFetchThreadPool;
static {
sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
}
private class DeleteClickListener implements DialogInterface.OnClickListener {
public void onClick(DialogInterface dialog, int which) {
getContentResolver().delete(mSelectedContactUri, null, null);
}
}
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// Resolve the intent
final Intent intent = getIntent();
// Allow the title to be set to a custom String using an extra on the intent
String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY);
if (title != null) {
setTitle(title);
}
final String action = intent.getAction();
mMode = MODE_UNKNOWN;
Log.i(TAG, "Called with action: " + action);
if (UI.LIST_DEFAULT.equals(action)) {
mMode = MODE_DEFAULT;
// When mDefaultMode is true the mode is set in onResume(), since the preferneces
// activity may change it whenever this activity isn't running
} else if (UI.LIST_GROUP_ACTION.equals(action)) {
mMode = MODE_GROUP;
String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
if (TextUtils.isEmpty(groupName)) {
finish();
return;
}
buildUserGroupUri(groupName);
} else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
mMode = MODE_CUSTOM;
mDisplayOnlyPhones = false;
} else if (UI.LIST_STARRED_ACTION.equals(action)) {
mMode = MODE_STARRED;
} else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
mMode = MODE_FREQUENT;
} else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
mMode = MODE_STREQUENT;
} else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
mMode = MODE_CUSTOM;
mDisplayOnlyPhones = true;
} else if (Intent.ACTION_PICK.equals(action)) {
// XXX These should be showing the data from the URI given in
// the Intent.
final String type = intent.resolveType(this);
if (Contacts.CONTENT_TYPE.equals(type)) {
mMode = MODE_PICK_CONTACT;
} else if (People.CONTENT_TYPE.equals(type)) {
mMode = MODE_LEGACY_PICK_PERSON;
} else if (Phone.CONTENT_TYPE.equals(type)) {
mMode = MODE_PICK_PHONE;
} else if (Phones.CONTENT_TYPE.equals(type)) {
mMode = MODE_LEGACY_PICK_PHONE;
} else if (StructuredPostal.CONTENT_TYPE.equals(type)) {
mMode = MODE_PICK_POSTAL;
} else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
mMode = MODE_LEGACY_PICK_POSTAL;
}
} else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
if (intent.getComponent().getClassName().equals("alias.DialShortcut")) {
mMode = MODE_PICK_PHONE;
mShortcutAction = Intent.ACTION_CALL;
setTitle(R.string.callShortcutActivityTitle);
} else if (intent.getComponent().getClassName().equals("alias.MessageShortcut")) {
mMode = MODE_PICK_PHONE;
mShortcutAction = Intent.ACTION_SENDTO;
setTitle(R.string.messageShortcutActivityTitle);
} else {
mMode = MODE_PICK_OR_CREATE_CONTACT;
mShortcutAction = Intent.ACTION_VIEW;
setTitle(R.string.shortcutActivityTitle);
}
} else if (Intent.ACTION_GET_CONTENT.equals(action)) {
final String type = intent.resolveType(this);
if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
mMode = MODE_PICK_OR_CREATE_CONTACT;
} else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
mMode = MODE_PICK_PHONE;
} else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
mMode = MODE_LEGACY_PICK_PHONE;
} else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
mMode = MODE_PICK_POSTAL;
} else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
mMode = MODE_LEGACY_PICK_POSTAL;
} else if (People.CONTENT_ITEM_TYPE.equals(type)) {
mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON;
}
} else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
mMode = MODE_INSERT_OR_EDIT_CONTACT;
} else if (Intent.ACTION_SEARCH.equals(action)) {
// See if the suggestion was clicked with a search action key (call button)
if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
String query = intent.getStringExtra(SearchManager.QUERY);
if (!TextUtils.isEmpty(query)) {
Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts("tel", query, null));
startActivity(newIntent);
}
finish();
return;
}
// See if search request has extras to specify query
if (intent.hasExtra(Insert.EMAIL)) {
mMode = MODE_QUERY_PICK_TO_VIEW;
mQueryMode = QUERY_MODE_MAILTO;
mQueryData = intent.getStringExtra(Insert.EMAIL);
} else if (intent.hasExtra(Insert.PHONE)) {
mMode = MODE_QUERY_PICK_TO_VIEW;
mQueryMode = QUERY_MODE_TEL;
mQueryData = intent.getStringExtra(Insert.PHONE);
} else {
// Otherwise handle the more normal search case
mMode = MODE_QUERY;
mQueryData = getIntent().getStringExtra(SearchManager.QUERY);
}
// Since this is the filter activity it receives all intents
// dispatched from the SearchManager for security reasons
// so we need to re-dispatch from here to the intended target.
} else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
Uri data = intent.getData();
Uri telUri = null;
if (sContactsIdMatcher.match(data) == CONTACTS_ID) {
long contactId = Long.valueOf(data.getLastPathSegment());
final Cursor cursor = queryPhoneNumbers(contactId);
if (cursor != null) {
if (cursor.getCount() == 1 && cursor.moveToFirst()) {
int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER);
String phoneNumber = cursor.getString(phoneNumberIndex);
telUri = Uri.parse("tel:" + phoneNumber);
}
cursor.close();
}
}
// See if the suggestion was clicked with a search action key (call button)
Intent newIntent;
if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) {
newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri);
} else {
newIntent = new Intent(Intent.ACTION_VIEW, data);
}
startActivity(newIntent);
finish();
return;
} else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
startActivity(newIntent);
finish();
return;
} else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
// TODO actually support this in EditContactActivity.
String number = intent.getData().getSchemeSpecificPart();
Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
newIntent.putExtra(Intents.Insert.PHONE, number);
startActivity(newIntent);
finish();
return;
}
if (JOIN_AGGREGATE.equals(action)) {
mMode = MODE_JOIN_CONTACT;
mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1);
if (mQueryAggregateId == -1) {
Log.e(TAG, "Intent " + action + " is missing required extra: "
+ EXTRA_AGGREGATE_ID);
setResult(RESULT_CANCELED);
finish();
}
}
if (mMode == MODE_UNKNOWN) {
mMode = MODE_DEFAULT;
}
if (mMode == MODE_JOIN_CONTACT) {
setContentView(R.layout.contacts_list_content_join);
TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb);
String blurb = getString(R.string.blurbJoinContactDataWith,
getContactDisplayName(mQueryAggregateId));
blurbView.setText(blurb);
mJoinModeShowAllContacts = true;
} else {
setContentView(R.layout.contacts_list_content);
}
// Setup the UI
final ListView list = getListView();
// Tell list view to not show dividers. We'll do it ourself so that we can *not* show
// them when an A-Z headers is visible.
list.setDividerHeight(0);
list.setFocusable(true);
list.setOnCreateContextMenuListener(this);
if ((mMode & MODE_MASK_NO_FILTER) != MODE_MASK_NO_FILTER) {
list.setTextFilterEnabled(true);
}
if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
// Add the header for creating a new contact
final LayoutInflater inflater = getLayoutInflater();
View header = inflater.inflate(R.layout.create_new_contact, list, false);
list.addHeaderView(header);
}
// Set the proper empty string
setEmptyText();
mAdapter = new ContactItemListAdapter(this);
setListAdapter(mAdapter);
getListView().setOnScrollListener(mAdapter);
// We manually save/restore the listview state
list.setSaveEnabled(false);
mQueryHandler = new QueryHandler(this);
mJustCreated = true;
// TODO(jham) redesign this
mSyncEnabled = true;
// // Check to see if sync is enabled
// final ContentResolver resolver = getContentResolver();
// IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
// if (provider == null) {
// // No contacts provider, bail.
// finish();
// return;
// }
//
// try {
// ISyncAdapter sa = provider.getSyncAdapter();
// mSyncEnabled = sa != null;
// } catch (RemoteException e) {
// mSyncEnabled = false;
// } finally {
// resolver.releaseProvider(provider);
// }
}
private String getContactDisplayName(long contactId) {
String contactName = null;
Cursor c = getContentResolver().query(
ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
new String[] {Contacts.DISPLAY_NAME}, null, null, null);
try {
if (c != null && c.moveToFirst()) {
contactName = c.getString(0);
}
} finally {
if (c != null) {
c.close();
}
}
if (contactName == null) {
contactName = "";
}
return contactName;
}
private int[] mLocation = new int[2];
private Rect mRect = new Rect();
/** {@inheritDoc} */
public void onClick(View v) {
if (v.getId() == R.id.call_button) {
final int position = (Integer) v.getTag();
Cursor c = mAdapter.getCursor();
if (c != null) {
c.moveToPosition(position);
callContact(c);
}
}
}
private void setEmptyText() {
if (mMode == MODE_JOIN_CONTACT) {
return;
}
TextView empty = (TextView) findViewById(R.id.emptyText);
int gravity = Gravity.NO_GRAVITY;
if (mDisplayOnlyPhones) {
empty.setText(getText(R.string.noContactsWithPhoneNumbers));
gravity = Gravity.CENTER;
} else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) {
empty.setText(getText(R.string.noFavoritesHelpText));
} else if (mMode == MODE_QUERY) {
empty.setText(getText(R.string.noMatchingContacts));
} else {
boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE))
.hasIccCard();
if (hasSim) {
if (mSyncEnabled) {
empty.setText(getText(R.string.noContactsHelpTextWithSync));
} else {
empty.setText(getText(R.string.noContactsHelpText));
}
} else {
if (mSyncEnabled) {
empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync));
} else {
empty.setText(getText(R.string.noContactsNoSimHelpText));
}
}
}
empty.setGravity(gravity);
}
private void buildUserGroupUri(String group) {
mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group);
}
/**
* Sets the mode when the request is for "default"
*/
private void setDefaultMode() {
// Load the preferences
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
// Update the empty text view with the proper string, as the group may have changed
setEmptyText();
}
@Override
protected void onResume() {
super.onResume();
// Force cache to reload so we don't show stale photos.
if (mAdapter.mBitmapCache != null) {
mAdapter.mBitmapCache.clear();
}
mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
boolean runQuery = true;
Activity parent = getParent();
// Do this before setting the filter. The filter thread relies
// on some state that is initialized in setDefaultMode
if (mMode == MODE_DEFAULT) {
// If we're in default mode we need to possibly reset the mode due to a change
// in the preferences activity while we weren't running
setDefaultMode();
}
// See if we were invoked with a filter
if (parent != null && parent instanceof DialtactsActivity) {
String filterText = ((DialtactsActivity) parent).getAndClearFilterText();
if (filterText != null && filterText.length() > 0) {
getListView().setFilterText(filterText);
// Don't start a new query since it will conflict with the filter
runQuery = false;
} else if (mJustCreated) {
getListView().clearTextFilter();
}
}
if (mJustCreated && runQuery) {
// We need to start a query here the first time the activity is launched, as long
// as we aren't doing a filter.
startQuery();
}
mJustCreated = false;
}
@Override
protected void onRestart() {
super.onRestart();
// The cursor was killed off in onStop(), so we need to get a new one here
// We do not perform the query if a filter is set on the list because the
// filter will cause the query to happen anyway
if (TextUtils.isEmpty(getListView().getTextFilter())) {
startQuery();
} else {
// Run the filtered query on the adapter
((ContactItemListAdapter) getListAdapter()).onContentChanged();
}
}
@Override
protected void onSaveInstanceState(Bundle icicle) {
super.onSaveInstanceState(icicle);
// Save list state in the bundle so we can restore it after the QueryHandler has run
icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
icicle.putBoolean(FOCUS_KEY, mList.hasFocus());
}
@Override
protected void onRestoreInstanceState(Bundle icicle) {
super.onRestoreInstanceState(icicle);
// Retrieve list state. This will be applied after the QueryHandler has run
mListState = icicle.getParcelable(LIST_STATE_KEY);
mListHasFocus = icicle.getBoolean(FOCUS_KEY);
}
@Override
protected void onStop() {
super.onStop();
// We don't want the list to display the empty state, since when we resume it will still
// be there and show up while the new query is happening. After the async query finished
// in response to onRestart() setLoading(false) will be called.
mAdapter.setLoading(true);
mAdapter.setSuggestionsCursor(null);
mAdapter.changeCursor(null);
mAdapter.clearImageFetching();
if (mMode == MODE_QUERY) {
// Make sure the search box is closed
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchManager.stopSearch();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// If Contacts was invoked by another Activity simply as a way of
// picking a contact, don't show the options menu
if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
return false;
}
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.list, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
final boolean defaultMode = (mMode == MODE_DEFAULT);
menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_display_groups: {
final Intent intent = new Intent(this, DisplayGroupsActivity.class);
startActivityForResult(intent, SUBACTIVITY_DISPLAY_GROUP);
return true;
}
case R.id.menu_search: {
startSearch(null, false, null, false);
return true;
}
case R.id.menu_add: {
final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
startActivity(intent);
return true;
}
case R.id.menu_import_export: {
displayImportExportDialog();
return true;
}
case R.id.menu_accounts: {
final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
ContactsContract.AUTHORITY
});
startActivity(intent);
return true;
}
}
return false;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case R.string.import_from_sim:
case R.string.import_from_sdcard: {
return AccountSelectionUtil.getSelectAccountDialog(this, id);
}
case R.id.dialog_sdcard_not_found: {
return new AlertDialog.Builder(this)
.setTitle(R.string.no_sdcard_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.no_sdcard_message)
.setPositiveButton(android.R.string.ok, null).create();
}
case R.id.dialog_delete_contact_confirmation: {
return new AlertDialog.Builder(this)
.setTitle(R.string.deleteConfirmation_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.deleteConfirmation)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok,
new DeleteClickListener()).create();
}
case R.id.dialog_readonly_contact_hide_confirmation: {
return new AlertDialog.Builder(this)
.setTitle(R.string.deleteConfirmation_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.readOnlyContactWarning)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok,
new DeleteClickListener()).create();
}
case R.id.dialog_readonly_contact_delete_confirmation: {
return new AlertDialog.Builder(this)
.setTitle(R.string.deleteConfirmation_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.readOnlyContactDeleteConfirmation)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok,
new DeleteClickListener()).create();
}
case R.id.dialog_multiple_contact_delete_confirmation: {
return new AlertDialog.Builder(this)
.setTitle(R.string.deleteConfirmation_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.multipleContactDeleteConfirmation)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok,
new DeleteClickListener()).create();
}
}
return super.onCreateDialog(id);
}
/**
* Create a {@link Dialog} that allows the user to pick from a bulk import
* or bulk export task across all contacts.
*/
private void displayImportExportDialog() {
// Wrap our context to inflate list items using correct theme
final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
final Resources res = dialogContext.getResources();
final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Adapter that shows a list of string resources
final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this,
android.R.layout.simple_list_item_1) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1,
parent, false);
}
final int resId = this.getItem(position);
((TextView)convertView).setText(resId);
return convertView;
}
};
if (TelephonyManager.getDefault().hasIccCard()) {
adapter.add(R.string.import_from_sim);
}
if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) {
adapter.add(R.string.import_from_sdcard);
}
if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) {
adapter.add(R.string.export_to_sdcard);
}
final DialogInterface.OnClickListener clickListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
final int resId = adapter.getItem(which);
switch (resId) {
case R.string.import_from_sim:
case R.string.import_from_sdcard: {
handleImportRequest(resId);
break;
}
case R.string.export_to_sdcard: {
Context context = ContactsListActivity.this;
Intent exportIntent = new Intent(context, ExportVCardActivity.class);
context.startActivity(exportIntent);
break;
}
default: {
Log.e(TAG, "Unexpected resource: " +
getResources().getResourceEntryName(resId));
}
}
}
};
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_import_export)
.setNegativeButton(android.R.string.cancel, null)
.setSingleChoiceItems(adapter, -1, clickListener)
.show();
}
private void handleImportRequest(int resId) {
// There's three possibilities:
// - more than one accounts -> ask the user
// - just one account -> use the account without asking the user
// - no account -> use phone-local storage without asking the user
final Sources sources = Sources.getInstance(this);
final List<Account> accountList = sources.getAccounts(true);
final int size = accountList.size();
if (size > 1) {
showDialog(resId);
return;
}
AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null));
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
switch (requestCode) {
case SUBACTIVITY_NEW_CONTACT:
if (resultCode == RESULT_OK) {
returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
data.getData(), 0);
}
break;
case SUBACTIVITY_VIEW_CONTACT:
if (resultCode == RESULT_OK) {
mAdapter.notifyDataSetChanged();
}
break;
case SUBACTIVITY_DISPLAY_GROUP:
// Mark as just created so we re-run the view query
mJustCreated = true;
break;
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
// If Contacts was invoked by another Activity simply as a way of
// picking a contact, don't show the context menu
if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
return;
}
AdapterView.AdapterContextMenuInfo info;
try {
info = (AdapterView.AdapterContextMenuInfo) menuInfo;
} catch (ClassCastException e) {
Log.e(TAG, "bad menuInfo", e);
return;
}
Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
if (cursor == null) {
// For some reason the requested item isn't available, do nothing
return;
}
long id = info.id;
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id);
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
// Setup the menu header
menu.setHeaderTitle(cursor.getString(SUMMARY_NAME_COLUMN_INDEX));
// View contact details
menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
.setIntent(new Intent(Intent.ACTION_VIEW, contactUri));
if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
// Calling contact
menu.add(0, MENU_ITEM_CALL, 0,
getString(R.string.menu_call));
// Send SMS item
menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS));
}
// Star toggling
int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
if (starState == 0) {
menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
} else {
menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
}
// Contact editing
menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
.setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri));
menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterView.AdapterContextMenuInfo info;
try {
info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
} catch (ClassCastException e) {
Log.e(TAG, "bad menuInfo", e);
return false;
}
Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
switch (item.getItemId()) {
case MENU_ITEM_TOGGLE_STAR: {
// Toggle the star
ContentValues values = new ContentValues(1);
values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
final Uri selectedUri = this.getContactUri(info.position);
getContentResolver().update(selectedUri, values, null, null);
return true;
}
case MENU_ITEM_CALL: {
callContact(cursor);
return true;
}
case MENU_ITEM_SEND_SMS: {
smsContact(cursor);
return true;
}
case MENU_ITEM_DELETE: {
mSelectedContactUri = getContactUri(info.position);
doContactDelete();
return true;
}
}
return super.onContextItemSelected(item);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_CALL: {
if (callSelection()) {
return true;
}
break;
}
case KeyEvent.KEYCODE_DEL: {
final int position = getListView().getSelectedItemPosition();
if (position != ListView.INVALID_POSITION) {
mSelectedContactUri = getContactUri(position);
doContactDelete();
return true;
}
break;
}
}
return super.onKeyDown(keyCode, event);
}
/**
* Prompt the user before deleting the given {@link Contacts} entry.
*/
protected void doContactDelete() {
mReadOnlySourcesCnt = 0;
mWritableSourcesCnt = 0;
mWritableRawContactIds.clear();
if (mSelectedContactUri != null) {
Cursor c = getContentResolver().query(RawContacts.CONTENT_URI, RAW_CONTACTS_PROJECTION,
RawContacts.CONTACT_ID + "=" + ContentUris.parseId(mSelectedContactUri), null,
null);
Sources sources = Sources.getInstance(ContactsListActivity.this);
if (c != null) {
while (c.moveToNext()) {
final String accountType = c.getString(2);
final long rawContactId = c.getLong(0);
ContactsSource contactsSource = sources.getInflatedSource(accountType,
ContactsSource.LEVEL_SUMMARY);
if (contactsSource != null && contactsSource.readOnly) {
mReadOnlySourcesCnt += 1;
} else {
mWritableSourcesCnt += 1;
mWritableRawContactIds.add(rawContactId);
}
}
}
c.close();
if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt > 0) {
showDialog(R.id.dialog_readonly_contact_delete_confirmation);
} else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
showDialog(R.id.dialog_readonly_contact_hide_confirmation);
} else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
showDialog(R.id.dialog_multiple_contact_delete_confirmation);
} else {
showDialog(R.id.dialog_delete_contact_confirmation);
}
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
// Hide soft keyboard, if visible
InputMethodManager inputMethodManager = (InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
if (mMode == MODE_INSERT_OR_EDIT_CONTACT) {
Intent intent;
if (position == 0) {
intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
} else {
// Edit. adjusting position by subtracting header view count.
position -= getListView().getHeaderViewsCount();
final Uri uri = getSelectedUri(position);
intent = new Intent(Intent.ACTION_EDIT, uri);
}
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
Bundle extras = getIntent().getExtras();
if (extras == null) {
extras = new Bundle();
}
intent.putExtras(extras);
extras.putBoolean(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER);
startActivity(intent);
finish();
} else if (id != -1) {
// Subtract one if we have Create Contact at the top
if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
position--;
}
final Uri uri = getSelectedUri(position);
if ((mMode & MODE_MASK_PICKER) == 0) {
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
} else if (mMode == MODE_JOIN_CONTACT) {
if (id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
mJoinModeShowAllContacts = false;
startQuery();
} else {
returnPickerResult(null, null, uri, id);
}
} else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
// Started with query that should launch to view contact
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
finish();
} else if (mMode == MODE_PICK_CONTACT
|| mMode == MODE_PICK_OR_CREATE_CONTACT
|| mMode == MODE_LEGACY_PICK_PERSON
|| mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) {
if (mShortcutAction != null) {
Cursor c = (Cursor) mAdapter.getItem(position);
returnPickerResult(c, c.getString(SUMMARY_NAME_COLUMN_INDEX), uri, id);
} else {
returnPickerResult(null, null, uri, id);
}
} else if (mMode == MODE_PICK_PHONE) {
if (mShortcutAction != null) {
Cursor c = (Cursor) mAdapter.getItem(position);
returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX), uri, id);
} else {
returnPickerResult(null, null, uri, id);
}
} else if (mMode == MODE_PICK_POSTAL
|| mMode == MODE_LEGACY_PICK_POSTAL
|| mMode == MODE_LEGACY_PICK_PHONE) {
returnPickerResult(null, null, uri, id);
}
} else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
&& position == 0) {
Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI);
startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
} else {
signalError();
}
}
/**
* @param uri In most cases, this should be a lookup {@link Uri}, possibly
* generated through {@link Contacts#getLookupUri(long, String)}.
*/
private void returnPickerResult(Cursor c, String name, Uri uri, long id) {
final Intent intent = new Intent();
if (mShortcutAction != null) {
Intent shortcutIntent;
if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
// This is a simple shortcut to view a contact.
shortcutIntent = new Intent(mShortcutAction, uri);
final Bitmap icon = loadContactPhoto(id, null);
if (icon != null) {
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
} else {
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(this,
R.drawable.ic_launcher_shortcut_contact));
}
} else {
// This is a direct dial or sms shortcut.
String number = c.getString(PHONE_NUMBER_COLUMN_INDEX);
int type = c.getInt(PHONE_TYPE_COLUMN_INDEX);
String scheme;
int resid;
if (Intent.ACTION_CALL.equals(mShortcutAction)) {
scheme = Constants.SCHEME_TEL;
resid = R.drawable.badge_action_call;
} else {
scheme = Constants.SCHEME_SMSTO;
resid = R.drawable.badge_action_sms;
}
// Make the URI a direct tel: URI so that it will always continue to work
Uri phoneUri = Uri.fromParts(scheme, number, null);
shortcutIntent = new Intent(mShortcutAction, phoneUri);
// Find the Contacts._ID for this phone number
long contactId = c.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
generatePhoneNumberIcon(contactId, type, resid));
}
shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
setResult(RESULT_OK, intent);
} else {
setResult(RESULT_OK, intent.setData(uri));
}
finish();
}
/**
* Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
* number, and if there is a photo also adds the call action icon.
*
* @param contactId The person the phone number belongs to
* @param type The type of the phone number
* @param actionResId The ID for the action resource
* @return The bitmap for the icon
*/
private Bitmap generatePhoneNumberIcon(long contactId, int type, int actionResId) {
final Resources r = getResources();
boolean drawPhoneOverlay = true;
final float scaleDensity = getResources().getDisplayMetrics().scaledDensity;
Bitmap photo = loadContactPhoto(contactId, null);
if (photo == null) {
// If there isn't a photo use the generic phone action icon instead
Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
if (phoneIcon != null) {
photo = phoneIcon;
drawPhoneOverlay = false;
} else {
return null;
}
}
// Setup the drawing classes
int iconSize = (int) r.getDimension(android.R.dimen.app_icon_size);
Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(icon);
// Copy in the photo
Paint photoPaint = new Paint();
photoPaint.setDither(true);
photoPaint.setFilterBitmap(true);
Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
Rect dst = new Rect(0,0, iconSize,iconSize);
canvas.drawBitmap(photo, src, dst, photoPaint);
// Create an overlay for the phone number type
String overlay = null;
switch (type) {
case Phone.TYPE_HOME:
overlay = getString(R.string.type_short_home);
break;
case Phone.TYPE_MOBILE:
overlay = getString(R.string.type_short_mobile);
break;
case Phone.TYPE_WORK:
overlay = getString(R.string.type_short_work);
break;
case Phone.TYPE_PAGER:
overlay = getString(R.string.type_short_pager);
break;
case Phone.TYPE_OTHER:
overlay = getString(R.string.type_short_other);
break;
}
if (overlay != null) {
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
textPaint.setTextSize(20.0f * scaleDensity);
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint);
}
// Draw the phone action icon as an overlay
if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) {
Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
if (phoneIcon != null) {
src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
int iconWidth = icon.getWidth();
dst.set(iconWidth - ((int) (20 * scaleDensity)), -1,
iconWidth, ((int) (19 * scaleDensity)));
canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
}
}
return icon;
}
/**
* Returns the icon for the phone call action.
*
* @param r The resources to load the icon from
* @param resId The resource ID to load
* @return the icon for the phone call action
*/
private Bitmap getPhoneActionIcon(Resources r, int resId) {
Drawable phoneIcon = r.getDrawable(resId);
if (phoneIcon instanceof BitmapDrawable) {
BitmapDrawable bd = (BitmapDrawable) phoneIcon;
return bd.getBitmap();
} else {
return null;
}
}
Uri getUriToQuery() {
switch(mMode) {
case MODE_JOIN_CONTACT:
return getJoinSuggestionsUri(null);
case MODE_FREQUENT:
case MODE_STARRED:
case MODE_DEFAULT:
case MODE_INSERT_OR_EDIT_CONTACT:
case MODE_PICK_CONTACT:
case MODE_PICK_OR_CREATE_CONTACT:{
return Contacts.CONTENT_URI;
}
case MODE_STREQUENT: {
return Contacts.CONTENT_STREQUENT_URI;
}
case MODE_LEGACY_PICK_PERSON:
case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
return People.CONTENT_URI;
}
case MODE_PICK_PHONE: {
return Phone.CONTENT_URI;
}
case MODE_LEGACY_PICK_PHONE: {
return Phones.CONTENT_URI;
}
case MODE_PICK_POSTAL: {
return StructuredPostal.CONTENT_URI;
}
case MODE_LEGACY_PICK_POSTAL: {
return ContactMethods.CONTENT_URI;
}
case MODE_QUERY_PICK_TO_VIEW: {
if (mQueryMode == QUERY_MODE_MAILTO) {
return Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(mQueryData));
} else if (mQueryMode == QUERY_MODE_TEL) {
return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(mQueryData));
}
}
case MODE_QUERY: {
return getContactFilterUri(mQueryData);
}
case MODE_GROUP: {
return mGroupUri;
}
default: {
throw new IllegalStateException("Can't generate URI: Unsupported Mode.");
}
}
}
/**
* Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given
* {@link ListView} position, using {@link #mAdapter}.
*/
private Uri getContactUri(int position) {
if (position == ListView.INVALID_POSITION) {
throw new IllegalArgumentException("Position not in list bounds");
}
final Cursor cursor = (Cursor)mAdapter.getItem(position);
switch(mMode) {
case MODE_LEGACY_PICK_PERSON:
case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
return ContentUris.withAppendedId(People.CONTENT_URI, personId);
}
default: {
// Build and return soft, lookup reference
final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY);
return Contacts.getLookupUri(contactId, lookupKey);
}
}
}
/**
* Build the {@link Uri} for the given {@link ListView} position, which can
* be used as result when in {@link #MODE_MASK_PICKER} mode.
*/
private Uri getSelectedUri(int position) {
if (position == ListView.INVALID_POSITION) {
throw new IllegalArgumentException("Position not in list bounds");
}
final long id = mAdapter.getItemId(position);
switch(mMode) {
case MODE_LEGACY_PICK_PERSON:
case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
return ContentUris.withAppendedId(People.CONTENT_URI, id);
}
case MODE_PICK_PHONE: {
return ContentUris.withAppendedId(Data.CONTENT_URI, id);
}
case MODE_LEGACY_PICK_PHONE: {
return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
}
case MODE_PICK_POSTAL: {
return ContentUris.withAppendedId(Data.CONTENT_URI, id);
}
case MODE_LEGACY_PICK_POSTAL: {
return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
}
default: {
return getContactUri(position);
}
}
}
String[] getProjectionForQuery() {
switch(mMode) {
case MODE_JOIN_CONTACT:
case MODE_STREQUENT:
case MODE_FREQUENT:
case MODE_STARRED:
case MODE_QUERY:
case MODE_DEFAULT:
case MODE_INSERT_OR_EDIT_CONTACT:
case MODE_GROUP:
case MODE_PICK_CONTACT:
case MODE_PICK_OR_CREATE_CONTACT: {
return CONTACTS_SUMMARY_PROJECTION;
}
case MODE_LEGACY_PICK_PERSON:
case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
return LEGACY_PEOPLE_PROJECTION ;
}
case MODE_PICK_PHONE: {
return PHONES_PROJECTION;
}
case MODE_LEGACY_PICK_PHONE: {
return LEGACY_PHONES_PROJECTION;
}
case MODE_PICK_POSTAL: {
return POSTALS_PROJECTION;
}
case MODE_LEGACY_PICK_POSTAL: {
return LEGACY_POSTALS_PROJECTION;
}
case MODE_QUERY_PICK_TO_VIEW: {
if (mQueryMode == QUERY_MODE_MAILTO) {
return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL;
} else if (mQueryMode == QUERY_MODE_TEL) {
return PHONES_PROJECTION;
}
break;
}
}
// Default to normal aggregate projection
return CONTACTS_SUMMARY_PROJECTION;
}
private Bitmap loadContactPhoto(long contactId, BitmapFactory.Options options) {
Cursor cursor = null;
Bitmap bm = null;
try {
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY);
cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO},
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
bm = ContactsUtils.loadContactPhoto(cursor, 0, options);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return bm;
}
/**
* Return the selection arguments for a default query based on
* {@link #mDisplayAll} and {@link #mDisplayOnlyPhones} flags.
*/
private String getContactSelection() {
if (mDisplayOnlyPhones) {
return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
} else {
return CLAUSE_ONLY_VISIBLE;
}
}
private Uri getContactFilterUri(String filter) {
if (!TextUtils.isEmpty(filter)) {
return Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
} else {
return Contacts.CONTENT_URI;
}
}
private Uri getPeopleFilterUri(String filter) {
if (!TextUtils.isEmpty(filter)) {
return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
} else {
return People.CONTENT_URI;
}
}
private Uri getJoinSuggestionsUri(String filter) {
Builder builder = Contacts.CONTENT_URI.buildUpon();
builder.appendEncodedPath(String.valueOf(mQueryAggregateId));
builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
if (!TextUtils.isEmpty(filter)) {
builder.appendEncodedPath(Uri.encode(filter));
}
builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
return builder.build();
}
private static String getSortOrder(String[] projectionType) {
/* if (Locale.getDefault().equals(Locale.JAPAN) &&
projectionType == AGGREGATES_PRIMARY_PHONE_PROJECTION) {
return SORT_STRING + " ASC";
} else {
return NAME_COLUMN + " COLLATE LOCALIZED ASC";
} */
return NAME_COLUMN + " COLLATE LOCALIZED ASC";
}
void startQuery() {
mAdapter.setLoading(true);
// Cancel any pending queries
mQueryHandler.cancelOperation(QUERY_TOKEN);
mQueryHandler.setLoadingJoinSuggestions(false);
String[] projection = getProjectionForQuery();
String callingPackage = getCallingPackage();
Uri uri = getUriToQuery();
if (!TextUtils.isEmpty(callingPackage)) {
uri = uri.buildUpon()
.appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY,
callingPackage)
.build();
}
// Kick off the new query
switch (mMode) {
case MODE_GROUP:
mQueryHandler.startQuery(QUERY_TOKEN, null,
uri, projection, getContactSelection(), null,
getSortOrder(projection));
break;
case MODE_DEFAULT:
case MODE_PICK_CONTACT:
case MODE_PICK_OR_CREATE_CONTACT:
case MODE_INSERT_OR_EDIT_CONTACT:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection, getContactSelection(), null,
getSortOrder(projection));
break;
case MODE_LEGACY_PICK_PERSON:
case MODE_LEGACY_PICK_OR_CREATE_PERSON:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection, null, null,
getSortOrder(projection));
break;
case MODE_QUERY: {
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection, null, null,
getSortOrder(projection));
break;
}
case MODE_QUERY_PICK_TO_VIEW: {
mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
getSortOrder(projection));
break;
}
case MODE_STARRED:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection, Contacts.STARRED + "=1", null,
getSortOrder(projection));
break;
case MODE_FREQUENT:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection,
Contacts.TIMES_CONTACTED + " > 0", null,
Contacts.TIMES_CONTACTED + " DESC, "
+ getSortOrder(projection));
break;
case MODE_STREQUENT:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null);
break;
case MODE_PICK_PHONE:
case MODE_LEGACY_PICK_PHONE:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection, null, null, getSortOrder(projection));
break;
case MODE_PICK_POSTAL:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection, null, null, getSortOrder(projection));
break;
case MODE_LEGACY_PICK_POSTAL:
mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
projection,
ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null,
getSortOrder(projection));
break;
case MODE_JOIN_CONTACT:
mQueryHandler.setLoadingJoinSuggestions(true);
mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection,
null, null, null);
break;
}
}
/**
* Called from a background thread to do the filter and return the resulting cursor.
*
* @param filter the text that was entered to filter on
* @return a cursor with the results of the filter
*/
Cursor doFilter(String filter) {
final ContentResolver resolver = getContentResolver();
String[] projection = getProjectionForQuery();
switch (mMode) {
case MODE_DEFAULT:
case MODE_PICK_CONTACT:
case MODE_PICK_OR_CREATE_CONTACT:
case MODE_INSERT_OR_EDIT_CONTACT: {
return resolver.query(getContactFilterUri(filter), projection,
getContactSelection(), null, getSortOrder(projection));
}
case MODE_LEGACY_PICK_PERSON:
case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
return resolver.query(getPeopleFilterUri(filter), projection, null, null,
getSortOrder(projection));
}
case MODE_STARRED: {
return resolver.query(getContactFilterUri(filter), projection,
Contacts.STARRED + "=1", null,
getSortOrder(projection));
}
case MODE_FREQUENT: {
return resolver.query(getContactFilterUri(filter), projection,
Contacts.TIMES_CONTACTED + " > 0", null,
Contacts.TIMES_CONTACTED + " DESC, "
+ getSortOrder(projection));
}
case MODE_STREQUENT: {
Uri uri;
if (!TextUtils.isEmpty(filter)) {
uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI,
Uri.encode(filter));
} else {
uri = Contacts.CONTENT_STREQUENT_URI;
}
return resolver.query(uri, projection, null, null, null);
}
case MODE_PICK_PHONE: {
Uri uri = getUriToQuery();
if (!TextUtils.isEmpty(filter)) {
uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter));
}
return resolver.query(uri, projection, null, null,
getSortOrder(projection));
}
case MODE_LEGACY_PICK_PHONE: {
//TODO: Support filtering here (bug 2092503)
break;
}
case MODE_JOIN_CONTACT: {
// We are on a background thread. Run queries one after the other synchronously
Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null,
null, null);
mAdapter.setSuggestionsCursor(cursor);
mJoinModeShowAllContacts = false;
return resolver.query(getContactFilterUri(filter), projection,
Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE,
null, getSortOrder(projection));
}
}
throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
}
private Cursor getShowAllContactsLabelCursor(String[] projection) {
MatrixCursor matrixCursor = new MatrixCursor(projection);
Object[] row = new Object[projection.length];
// The only columns we care about is the id
row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID;
matrixCursor.addRow(row);
return matrixCursor;
}
/**
* Calls the currently selected list item.
* @return true if the call was initiated, false otherwise
*/
boolean callSelection() {
ListView list = getListView();
if (list.hasFocus()) {
Cursor cursor = (Cursor) list.getSelectedItem();
return callContact(cursor);
}
return false;
}
boolean callContact(Cursor cursor) {
return callOrSmsContact(cursor, false /*call*/);
}
boolean smsContact(Cursor cursor) {
return callOrSmsContact(cursor, true /*sms*/);
}
/**
* Calls the contact which the cursor is point to.
* @return true if the call was initiated, false otherwise
*/
boolean callOrSmsContact(Cursor cursor, boolean sendSms) {
if (cursor != null) {
boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
if (!hasPhone) {
// There is no phone number.
signalError();
return false;
}
String phone = null;
Cursor phonesCursor = null;
phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX));
if (phonesCursor == null || phonesCursor.getCount() == 0) {
// No valid number
signalError();
return false;
} else if (phonesCursor.getCount() == 1) {
// only one number, call it.
phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
} else {
phonesCursor.moveToPosition(-1);
while (phonesCursor.moveToNext()) {
if (phonesCursor.getInt(phonesCursor.
getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
// Found super primary, call it.
phone = phonesCursor.
getString(phonesCursor.getColumnIndex(Phone.NUMBER));
break;
}
}
}
if (phone == null) {
// Display dialog to choose a number to call.
PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(
this, phonesCursor, sendSms);
phoneDialog.show();
} else {
if (sendSms) {
ContactsUtils.initiateSms(this, phone);
} else {
ContactsUtils.initiateCall(this, phone);
}
}
return true;
}
return false;
}
private Cursor queryPhoneNumbers(long contactId) {
Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
Cursor c = getContentResolver().query(dataUri,
new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY},
Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null);
if (c != null && c.moveToFirst()) {
return c;
}
return null;
}
/**
* Signal an error to the user.
*/
void signalError() {
//TODO play an error beep or something...
}
Cursor getItemForView(View view) {
ListView listView = getListView();
int index = listView.getPositionForView(view);
if (index < 0) {
return null;
}
return (Cursor) listView.getAdapter().getItem(index);
}
private static class QueryHandler extends AsyncQueryHandler {
protected final WeakReference<ContactsListActivity> mActivity;
protected boolean mLoadingJoinSuggestions = false;
public QueryHandler(Context context) {
super(context.getContentResolver());
mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
}
public void setLoadingJoinSuggestions(boolean flag) {
mLoadingJoinSuggestions = flag;
}
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
final ContactsListActivity activity = mActivity.get();
if (activity != null && !activity.isFinishing()) {
// Whenever we get a suggestions cursor, we need to immediately kick off
// another query for the complete list of contacts
if (cursor != null && mLoadingJoinSuggestions) {
mLoadingJoinSuggestions = false;
if (cursor.getCount() > 0) {
activity.mAdapter.setSuggestionsCursor(cursor);
} else {
cursor.close();
activity.mAdapter.setSuggestionsCursor(null);
}
if (activity.mAdapter.mSuggestionsCursorCount == 0
|| !activity.mJoinModeShowAllContacts) {
startQuery(QUERY_TOKEN, null, activity.getContactFilterUri(
activity.mQueryData),
CONTACTS_SUMMARY_PROJECTION,
Contacts._ID + " != " + activity.mQueryAggregateId
+ " AND " + CLAUSE_ONLY_VISIBLE, null,
getSortOrder(CONTACTS_SUMMARY_PROJECTION));
return;
}
cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
}
activity.mAdapter.setLoading(false);
activity.getListView().clearTextFilter();
activity.mAdapter.changeCursor(cursor);
// Now that the cursor is populated again, it's possible to restore the list state
if (activity.mListState != null) {
activity.mList.onRestoreInstanceState(activity.mListState);
if (activity.mListHasFocus) {
activity.mList.requestFocus();
}
activity.mListHasFocus = false;
activity.mListState = null;
}
} else {
cursor.close();
}
}
}
final static class ContactListItemCache {
public View header;
public TextView headerText;
public View divider;
public TextView nameView;
public View callView;
public ImageView callButton;
public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
public TextView labelView;
public CharArrayBuffer labelBuffer = new CharArrayBuffer(128);
public TextView dataView;
public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
public ImageView presenceView;
public QuickContactBadge photoView;
public ImageView nonQuickContactPhotoView;
}
final static class PhotoInfo {
public int position;
public long photoId;
public PhotoInfo(int position, long photoId) {
this.position = position;
this.photoId = photoId;
}
public QuickContactBadge photoView;
}
private final class ContactItemListAdapter extends ResourceCursorAdapter
implements SectionIndexer, OnScrollListener {
private SectionIndexer mIndexer;
private String mAlphabet;
private boolean mLoading = true;
private CharSequence mUnknownNameText;
private boolean mDisplayPhotos = false;
private boolean mDisplayCallButton = false;
private boolean mDisplayAdditionalData = true;
private HashMap<Long, SoftReference<Bitmap>> mBitmapCache = null;
private HashSet<ImageView> mItemsMissingImages = null;
private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
private boolean mDisplaySectionHeaders = true;
private int[] mSectionPositions;
private Cursor mSuggestionsCursor;
private int mSuggestionsCursorCount;
private ImageFetchHandler mHandler;
private ImageDbFetcher mImageFetcher;
private static final int FETCH_IMAGE_MSG = 1;
public ContactItemListAdapter(Context context) {
super(context, R.layout.contacts_list_item, null, false);
mHandler = new ImageFetchHandler();
mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
mUnknownNameText = context.getText(android.R.string.unknownName);
switch (mMode) {
case MODE_LEGACY_PICK_POSTAL:
case MODE_PICK_POSTAL:
mDisplaySectionHeaders = false;
break;
case MODE_LEGACY_PICK_PHONE:
case MODE_PICK_PHONE:
mDisplaySectionHeaders = false;
break;
default:
break;
}
// Do not display the second line of text if in a specific SEARCH query mode, usually for
// matching a specific E-mail or phone number. Any contact details
// shown would be identical, and columns might not even be present
// in the returned cursor.
if (mQueryMode != QUERY_MODE_NONE) {
mDisplayAdditionalData = false;
}
if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) {
mDisplayAdditionalData = false;
}
if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) {
mDisplayCallButton = true;
}
if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
mDisplayPhotos = true;
setViewResource(R.layout.contacts_list_item_photo);
mBitmapCache = new HashMap<Long, SoftReference<Bitmap>>();
mItemsMissingImages = new HashSet<ImageView>();
}
if (mMode == MODE_STREQUENT || mMode == MODE_FREQUENT) {
mDisplaySectionHeaders = false;
}
}
private class ImageFetchHandler extends Handler {
@Override
public void handleMessage(Message message) {
if (ContactsListActivity.this.isFinishing()) {
return;
}
switch(message.what) {
case FETCH_IMAGE_MSG: {
final ImageView imageView = (ImageView) message.obj;
if (imageView == null) {
break;
}
final PhotoInfo info = (PhotoInfo)imageView.getTag();
if (info == null) {
break;
}
final long photoId = info.photoId;
if (photoId == 0) {
break;
}
SoftReference<Bitmap> photoRef = mBitmapCache.get(photoId);
if (photoRef == null) {
break;
}
Bitmap photo = photoRef.get();
if (photo == null) {
mBitmapCache.remove(photoId);
break;
}
// Make sure the photoId on this image view has not changed
// while we were loading the image.
synchronized (imageView) {
final PhotoInfo updatedInfo = (PhotoInfo)imageView.getTag();
long currentPhotoId = updatedInfo.photoId;
if (currentPhotoId == photoId) {
imageView.setImageBitmap(photo);
mItemsMissingImages.remove(imageView);
}
}
break;
}
}
}
public void clearImageFecthing() {
removeMessages(FETCH_IMAGE_MSG);
}
}
private class ImageDbFetcher implements Runnable {
long mPhotoId;
private ImageView mImageView;
public ImageDbFetcher(long photoId, ImageView imageView) {
this.mPhotoId = photoId;
this.mImageView = imageView;
}
public void run() {
if (ContactsListActivity.this.isFinishing()) {
return;
}
if (Thread.currentThread().interrupted()) {
// shutdown has been called.
return;
}
Bitmap photo = null;
try {
photo = ContactsUtils.loadContactPhoto(mContext, mPhotoId, null);
} catch (OutOfMemoryError e) {
// Not enough memory for the photo, do nothing.
}
if (photo == null) {
return;
}
mBitmapCache.put(mPhotoId, new SoftReference<Bitmap>(photo));
if (Thread.currentThread().interrupted()) {
// shutdown has been called.
return;
}
// Update must happen on UI thread
Message msg = new Message();
msg.what = FETCH_IMAGE_MSG;
msg.obj = mImageView;
mHandler.sendMessage(msg);
}
}
public void setSuggestionsCursor(Cursor cursor) {
if (mSuggestionsCursor != null) {
mSuggestionsCursor.close();
}
mSuggestionsCursor = cursor;
mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
}
private SectionIndexer getNewIndexer(Cursor cursor) {
/* if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) {
return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX);
} else { */
return new AlphabetIndexer(cursor, SUMMARY_NAME_COLUMN_INDEX, mAlphabet);
/* } */
}
/**
* Callback on the UI thread when the content observer on the backing cursor fires.
* Instead of calling requery we need to do an async query so that the requery doesn't
* block the UI thread for a long time.
*/
@Override
protected void onContentChanged() {
CharSequence constraint = getListView().getTextFilter();
if (!TextUtils.isEmpty(constraint)) {
// Reset the filter state then start an async filter operation
Filter filter = getFilter();
filter.filter(constraint);
} else {
// Start an async query
startQuery();
}
}
public void setLoading(boolean loading) {
mLoading = loading;
}
@Override
public boolean isEmpty() {
if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
// This mode mask adds a header and we always want it to show up, even
// if the list is empty, so always claim the list is not empty.
return false;
} else {
if (mLoading) {
// We don't want the empty state to show when loading.
return false;
} else {
return super.isEmpty();
}
}
}
@Override
public int getItemViewType(int position) {
if (position == 0 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
return IGNORE_ITEM_VIEW_TYPE;
}
if (isShowAllContactsItemPosition(position)) {
return IGNORE_ITEM_VIEW_TYPE;
}
if (getSeparatorId(position) != 0) {
// We don't want the separator view to be recycled.
return IGNORE_ITEM_VIEW_TYPE;
}
return super.getItemViewType(position);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (!mDataValid) {
throw new IllegalStateException(
"this should only be called when the cursor is valid");
}
// handle the total contacts item
if (position == 0 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
return getTotalContactCountView(parent);
}
if (isShowAllContactsItemPosition(position)) {
LayoutInflater inflater =
(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return inflater.inflate(R.layout.contacts_list_show_all_item, parent, false);
}
// Handle the separator specially
int separatorId = getSeparatorId(position);
if (separatorId != 0) {
LayoutInflater inflater =
(LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
view.setText(separatorId);
return view;
}
boolean showingSuggestion;
Cursor cursor;
if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
showingSuggestion = true;
cursor = mSuggestionsCursor;
} else {
showingSuggestion = false;
cursor = mCursor;
}
int realPosition = getRealPosition(position);
if (!cursor.moveToPosition(realPosition)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
View v;
if (convertView == null) {
v = newView(mContext, cursor, parent);
} else {
v = convertView;
}
bindView(v, mContext, cursor);
bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion);
return v;
}
private View getTotalContactCountView(ViewGroup parent) {
final LayoutInflater inflater = getLayoutInflater();
TextView totalContacts = (TextView) inflater.inflate(R.layout.total_contacts,
parent, false);
String text;
int count = getRealCount();
if (mMode == MODE_QUERY || !TextUtils.isEmpty(getListView().getTextFilter())) {
text = getQuantityText(count, R.string.listFoundAllContactsZero,
R.plurals.listFoundAllContacts);
} else {
if (mDisplayOnlyPhones) {
text = getQuantityText(count, R.string.listTotalPhoneContactsZero,
R.plurals.listTotalPhoneContacts);
} else {
text = getQuantityText(count, R.string.listTotalAllContactsZero,
R.plurals.listTotalAllContacts);
}
}
totalContacts.setText(text);
return totalContacts;
}
// TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
private String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
if (count == 0) {
return getString(zeroResourceId);
} else {
String format = getResources().getQuantityText(pluralResourceId, count).toString();
return String.format(format, count);
}
}
private boolean isShowAllContactsItemPosition(int position) {
return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts
&& mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2;
}
private int getSeparatorId(int position) {
int separatorId = 0;
if (position == mFrequentSeparatorPos) {
separatorId = R.string.favoritesFrquentSeparator;
}
if (mSuggestionsCursorCount != 0) {
if (position == 0) {
separatorId = R.string.separatorJoinAggregateSuggestions;
} else if (position == mSuggestionsCursorCount + 1) {
separatorId = R.string.separatorJoinAggregateAll;
}
}
return separatorId;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
final View view = super.newView(context, cursor, parent);
final ContactListItemCache cache = new ContactListItemCache();
cache.header = view.findViewById(R.id.header);
cache.headerText = (TextView)view.findViewById(R.id.header_text);
cache.divider = view.findViewById(R.id.list_divider);
cache.nameView = (TextView) view.findViewById(R.id.name);
cache.callView = view.findViewById(R.id.call_view);
cache.callButton = (ImageView) view.findViewById(R.id.call_button);
if (cache.callButton != null) {
cache.callButton.setOnClickListener(ContactsListActivity.this);
}
cache.labelView = (TextView) view.findViewById(R.id.label);
cache.dataView = (TextView) view.findViewById(R.id.data);
cache.presenceView = (ImageView) view.findViewById(R.id.presence);
cache.photoView = (QuickContactBadge) view.findViewById(R.id.photo);
cache.nonQuickContactPhotoView = (ImageView) view.findViewById(R.id.noQuickContactPhoto);
view.setTag(cache);
return view;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
final ContactListItemCache cache = (ContactListItemCache) view.getTag();
TextView dataView = cache.dataView;
TextView labelView = cache.labelView;
int typeColumnIndex;
int dataColumnIndex;
int labelColumnIndex;
int defaultType;
int nameColumnIndex;
boolean displayAdditionalData = mDisplayAdditionalData;
switch(mMode) {
case MODE_PICK_PHONE:
case MODE_LEGACY_PICK_PHONE: {
nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX;
dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX;
typeColumnIndex = PHONE_TYPE_COLUMN_INDEX;
labelColumnIndex = PHONE_LABEL_COLUMN_INDEX;
defaultType = Phone.TYPE_HOME;
break;
}
case MODE_PICK_POSTAL:
case MODE_LEGACY_PICK_POSTAL: {
nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX;
dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX;
typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX;
labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX;
defaultType = StructuredPostal.TYPE_HOME;
break;
}
default: {
nameColumnIndex = SUMMARY_NAME_COLUMN_INDEX;
dataColumnIndex = -1;
typeColumnIndex = -1;
labelColumnIndex = -1;
defaultType = Phone.TYPE_HOME;
displayAdditionalData = false;
}
}
// Set the name
cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
int size = cache.nameBuffer.sizeCopied;
if (size != 0) {
cache.nameView.setText(cache.nameBuffer.data, 0, size);
} else {
cache.nameView.setText(mUnknownNameText);
}
// Make the call button visible if requested.
if (mDisplayCallButton) {
int pos = cursor.getPosition();
cache.callView.setVisibility(View.VISIBLE);
cache.callButton.setTag(pos);
} else {
cache.callView.setVisibility(View.GONE);
}
// Set the photo, if requested
if (mDisplayPhotos) {
boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0;
long photoId = 0;
if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX);
}
ImageView viewToUse;
if (useQuickContact) {
viewToUse = cache.photoView;
// Build soft lookup reference
final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY);
cache.photoView.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
cache.photoView.setVisibility(View.VISIBLE);
cache.nonQuickContactPhotoView.setVisibility(View.INVISIBLE);
} else {
viewToUse = cache.nonQuickContactPhotoView;
cache.photoView.setVisibility(View.INVISIBLE);
cache.nonQuickContactPhotoView.setVisibility(View.VISIBLE);
}
final int position = cursor.getPosition();
viewToUse.setTag(new PhotoInfo(position, photoId));
if (photoId == 0) {
viewToUse.setImageResource(R.drawable.ic_contact_list_picture);
} else {
Bitmap photo = null;
// Look for the cached bitmap
SoftReference<Bitmap> ref = mBitmapCache.get(photoId);
if (ref != null) {
photo = ref.get();
if (photo == null) {
mBitmapCache.remove(photoId);
}
}
// Bind the photo, or use the fallback no photo resource
if (photo != null) {
viewToUse.setImageBitmap(photo);
} else {
// Cache miss
viewToUse.setImageResource(R.drawable.ic_contact_list_picture);
// Add it to a set of images that are populated asynchronously.
mItemsMissingImages.add(viewToUse);
if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
// Scrolling is idle or slow, go get the image right now.
sendFetchImageMessage(viewToUse);
}
}
}
}
ImageView presenceView = cache.presenceView;
if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
// Set the proper icon (star or presence or nothing)
int serverStatus;
if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) {
serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX);
presenceView.setImageResource(
Presence.getPresenceIconResourceId(serverStatus));
presenceView.setVisibility(View.VISIBLE);
} else {
presenceView.setVisibility(View.GONE);
}
} else {
presenceView.setVisibility(View.GONE);
}
if (!displayAdditionalData) {
cache.dataView.setVisibility(View.GONE);
cache.labelView.setVisibility(View.GONE);
return;
}
// Set the data.
cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer);
size = cache.dataBuffer.sizeCopied;
if (size != 0) {
dataView.setText(cache.dataBuffer.data, 0, size);
dataView.setVisibility(View.VISIBLE);
} else {
dataView.setVisibility(View.GONE);
}
// Set the label.
if (!cursor.isNull(typeColumnIndex)) {
labelView.setVisibility(View.VISIBLE);
final int type = cursor.getInt(typeColumnIndex);
final String label = cursor.getString(labelColumnIndex);
if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
labelView.setText(StructuredPostal.getTypeLabel(context.getResources(), type,
label));
} else {
labelView.setText(Phone.getTypeLabel(context.getResources(), type, label));
}
} else {
// There is no label, hide the the view
labelView.setVisibility(View.GONE);
}
}
private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
final ContactListItemCache cache = (ContactListItemCache) view.getTag();
if (!displaySectionHeaders) {
cache.header.setVisibility(View.GONE);
cache.divider.setVisibility(View.VISIBLE);
} else {
final int section = getSectionForPosition(position);
if (getPositionForSection(section) == position) {
String title = mIndexer.getSections()[section].toString().trim();
if (!TextUtils.isEmpty(title)) {
cache.headerText.setText(title);
cache.header.setVisibility(View.VISIBLE);
} else {
cache.header.setVisibility(View.GONE);
}
} else {
cache.header.setVisibility(View.GONE);
}
// move the divider for the last item in a section
if (getPositionForSection(section + 1) - 1 == position) {
cache.divider.setVisibility(View.GONE);
} else {
cache.divider.setVisibility(View.VISIBLE);
}
}
}
@Override
public void changeCursor(Cursor cursor) {
// Get the split between starred and frequent items, if the mode is strequent
mFrequentSeparatorPos = ListView.INVALID_POSITION;
int cursorCount = 0;
if (cursor != null && (cursorCount = cursor.getCount()) > 0
&& mMode == MODE_STREQUENT) {
cursor.move(-1);
for (int i = 0; cursor.moveToNext(); i++) {
int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
if (starred == 0) {
if (i > 0) {
// Only add the separator when there are starred items present
mFrequentSeparatorPos = i;
}
break;
}
}
}
super.changeCursor(cursor);
// Update the indexer for the fast scroll widget
updateIndexer(cursor);
}
private void updateIndexer(Cursor cursor) {
if (mIndexer == null) {
mIndexer = getNewIndexer(cursor);
} else {
if (Locale.getDefault().equals(Locale.JAPAN)) {
if (mIndexer instanceof JapaneseContactListIndexer) {
((JapaneseContactListIndexer)mIndexer).setCursor(cursor);
} else {
mIndexer = getNewIndexer(cursor);
}
} else {
if (mIndexer instanceof AlphabetIndexer) {
((AlphabetIndexer)mIndexer).setCursor(cursor);
} else {
mIndexer = getNewIndexer(cursor);
}
}
}
int sectionCount = mIndexer.getSections().length;
if (mSectionPositions == null || mSectionPositions.length != sectionCount) {
mSectionPositions = new int[sectionCount];
}
for (int i = 0; i < sectionCount; i++) {
mSectionPositions[i] = ListView.INVALID_POSITION;
}
}
/**
* Run the query on a helper thread. Beware that this code does not run
* on the main UI thread!
*/
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
return doFilter(constraint.toString());
}
public Object [] getSections() {
if (mMode == MODE_STARRED) {
return new String[] { " " };
} else {
return mIndexer.getSections();
}
}
public int getPositionForSection(int sectionIndex) {
if (mMode == MODE_STARRED) {
return -1;
}
if (sectionIndex < 0 || sectionIndex >= mSectionPositions.length) {
return -1;
}
if (mIndexer == null) {
Cursor cursor = mAdapter.getCursor();
if (cursor == null) {
// No cursor, the section doesn't exist so just return 0
return 0;
}
mIndexer = getNewIndexer(cursor);
}
int position = mSectionPositions[sectionIndex];
if (position == ListView.INVALID_POSITION) {
position = mSectionPositions[sectionIndex] =
mIndexer.getPositionForSection(sectionIndex);
}
return position;
}
public int getSectionForPosition(int position) {
// The current implementations of SectionIndexers (specifically the Japanese indexer)
// only work in one direction: given a section they can calculate the position.
// Here we are using that existing functionality to do the reverse mapping. We are
// performing binary search in the mSectionPositions array, which itself is populated
// lazily using the "forward" mapping supported by the indexer.
int start = 0;
int end = mSectionPositions.length;
while (start != end) {
// We are making the binary search slightly asymmetrical, because the
// user is more likely to be scrolling the list from the top down.
int pivot = start + (end - start) / 4;
int value = getPositionForSection(pivot);
if (value <= position) {
start = pivot + 1;
} else {
end = pivot;
}
}
// The variable "start" cannot be 0, as long as the indexer is implemented properly
// and actually maps position = 0 to section = 0
return start - 1;
}
@Override
public boolean areAllItemsEnabled() {
return mMode != MODE_STARRED
&& (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) == 0
&& mSuggestionsCursorCount == 0;
}
@Override
public boolean isEnabled(int position) {
if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
if (position == 0) {
return false;
}
position--;
}
if (mSuggestionsCursorCount > 0) {
return position != 0 && position != mSuggestionsCursorCount + 1;
}
return position != mFrequentSeparatorPos;
}
@Override
public int getCount() {
if (!mDataValid) {
return 0;
}
int superCount = super.getCount();
if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 && superCount > 0) {
// We don't want to count this header if it's the only thing visible, so that
// the empty text will display.
superCount++;
}
if (mSuggestionsCursorCount != 0) {
// When showing suggestions, we have 2 additional list items: the "Suggestions"
// and "All contacts" headers.
return mSuggestionsCursorCount + superCount + 2;
}
else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
// When showing strequent list, we have an additional list item - the separator.
return superCount + 1;
} else {
return superCount;
}
}
/**
* Gets the actual count of contacts and excludes all the headers.
*/
public int getRealCount() {
return super.getCount();
}
private int getRealPosition(int pos) {
if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
pos--;
}
if (mSuggestionsCursorCount != 0) {
// When showing suggestions, we have 2 additional list items: the "Suggestions"
// and "All contacts" separators.
if (pos < mSuggestionsCursorCount + 2) {
// We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
// separator.
return pos - 1;
} else {
// We are in the lower partition (All contacts). Adjusting for the size
// of the upper partition plus the two separators.
return pos - mSuggestionsCursorCount - 2;
}
} else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
// No separator, identity map
return pos;
} else if (pos <= mFrequentSeparatorPos) {
// Before or at the separator, identity map
return pos;
} else {
// After the separator, remove 1 from the pos to get the real underlying pos
return pos - 1;
}
}
@Override
public Object getItem(int pos) {
if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
mSuggestionsCursor.moveToPosition(getRealPosition(pos));
return mSuggestionsCursor;
} else {
return super.getItem(getRealPosition(pos));
}
}
@Override
public long getItemId(int pos) {
if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) {
if (mSuggestionsCursor.moveToPosition(pos - 1)) {
return mSuggestionsCursor.getLong(mRowIDColumn);
} else {
return 0;
}
}
return super.getItemId(getRealPosition(pos));
}
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
// no op
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
mScrollState = scrollState;
if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
// If we are in a fling, stop loading images.
clearImageFetching();
} else if (mDisplayPhotos) {
processMissingImageItems(view);
}
}
private void processMissingImageItems(AbsListView view) {
for (ImageView iv : mItemsMissingImages) {
sendFetchImageMessage(iv);
}
}
private void sendFetchImageMessage(ImageView view) {
final PhotoInfo info = (PhotoInfo) view.getTag();
if (info == null) {
return;
}
final long photoId = info.photoId;
if (photoId == 0) {
return;
}
mImageFetcher = new ImageDbFetcher(photoId, view);
synchronized (ContactsListActivity.this) {
// can't sync on sImageFetchThreadPool.
if (sImageFetchThreadPool == null) {
// Don't use more than 3 threads at a time to update. The thread pool will be
// shared by all contact items.
sImageFetchThreadPool = Executors.newFixedThreadPool(3);
}
sImageFetchThreadPool.execute(mImageFetcher);
}
}
/**
* Stop the image fetching for ALL contacts, if one is in progress we'll
* not query the database.
*
* TODO: move this method to ContactsListActivity, it does not apply to the current
* contact.
*/
public void clearImageFetching() {
synchronized (ContactsListActivity.this) {
if (sImageFetchThreadPool != null) {
sImageFetchThreadPool.shutdownNow();
sImageFetchThreadPool = null;
}
}
mHandler.clearImageFecthing();
}
}
}