| /* |
| * Copyright (C) 2011 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.group; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.Photo; |
| import android.provider.ContactsContract.Contacts.Data; |
| import android.provider.ContactsContract.RawContacts; |
| import android.provider.ContactsContract.RawContactsEntity; |
| import android.text.TextUtils; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ArrayAdapter; |
| import android.widget.AutoCompleteTextView; |
| import android.widget.Filter; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import com.android.contacts.R; |
| import com.android.contacts.common.ContactPhotoManager; |
| import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * This adapter provides suggested contacts that can be added to a group for an |
| * {@link AutoCompleteTextView} within the group editor. |
| */ |
| public class SuggestedMemberListAdapter extends ArrayAdapter<SuggestedMember> { |
| |
| private static final String[] PROJECTION_FILTERED_MEMBERS = new String[] { |
| RawContacts._ID, // 0 |
| RawContacts.CONTACT_ID, // 1 |
| RawContacts.DISPLAY_NAME_PRIMARY // 2 |
| }; |
| |
| private static final int RAW_CONTACT_ID_COLUMN_INDEX = 0; |
| private static final int CONTACT_ID_COLUMN_INDEX = 1; |
| private static final int DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 2; |
| |
| private static final String[] PROJECTION_MEMBER_DATA = new String[] { |
| RawContacts._ID, // 0 |
| RawContacts.CONTACT_ID, // 1 |
| Data.MIMETYPE, // 2 |
| Data.DATA1, // 3 |
| Photo.PHOTO, // 4 |
| }; |
| |
| private static final int MIMETYPE_COLUMN_INDEX = 2; |
| private static final int DATA_COLUMN_INDEX = 3; |
| private static final int PHOTO_COLUMN_INDEX = 4; |
| |
| private Filter mFilter; |
| private ContentResolver mContentResolver; |
| private LayoutInflater mInflater; |
| |
| private String mAccountType; |
| private String mAccountName; |
| private String mDataSet; |
| |
| // TODO: Make this a Map for better performance when we check if a new contact is in the list |
| // or not |
| private final List<Long> mExistingMemberContactIds = new ArrayList<Long>(); |
| |
| private static final int SUGGESTIONS_LIMIT = 5; |
| |
| public SuggestedMemberListAdapter(Context context, int textViewResourceId) { |
| super(context, textViewResourceId); |
| mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| } |
| |
| public void setAccountType(String accountType) { |
| mAccountType = accountType; |
| } |
| |
| public void setAccountName(String accountName) { |
| mAccountName = accountName; |
| } |
| |
| public void setDataSet(String dataSet) { |
| mDataSet = dataSet; |
| } |
| |
| public void setContentResolver(ContentResolver resolver) { |
| mContentResolver = resolver; |
| } |
| |
| public void updateExistingMembersList(List<GroupEditorFragment.Member> list) { |
| mExistingMemberContactIds.clear(); |
| for (GroupEditorFragment.Member member : list) { |
| mExistingMemberContactIds.add(member.getContactId()); |
| } |
| } |
| |
| public void addNewMember(long contactId) { |
| mExistingMemberContactIds.add(contactId); |
| } |
| |
| public void removeMember(long contactId) { |
| if (mExistingMemberContactIds.contains(contactId)) { |
| mExistingMemberContactIds.remove(contactId); |
| } |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| View result = convertView; |
| if (result == null) { |
| result = mInflater.inflate(R.layout.group_member_suggestion, parent, false); |
| } |
| // TODO: Use a viewholder |
| SuggestedMember member = getItem(position); |
| TextView text1 = (TextView) result.findViewById(R.id.text1); |
| TextView text2 = (TextView) result.findViewById(R.id.text2); |
| ImageView icon = (ImageView) result.findViewById(R.id.icon); |
| text1.setText(member.getDisplayName()); |
| if (member.hasExtraInfo()) { |
| text2.setText(member.getExtraInfo()); |
| } else { |
| text2.setVisibility(View.GONE); |
| } |
| byte[] byteArray = member.getPhotoByteArray(); |
| if (byteArray == null) { |
| icon.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact( |
| icon.getResources(), false, null)); |
| } else { |
| Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); |
| icon.setImageBitmap(bitmap); |
| } |
| result.setTag(member); |
| return result; |
| } |
| |
| @Override |
| public Filter getFilter() { |
| if (mFilter == null) { |
| mFilter = new SuggestedMemberFilter(); |
| } |
| return mFilter; |
| } |
| |
| /** |
| * This filter queries for raw contacts that match the given account name and account type, |
| * as well as the search query. |
| */ |
| public class SuggestedMemberFilter extends Filter { |
| |
| @Override |
| protected FilterResults performFiltering(CharSequence prefix) { |
| FilterResults results = new FilterResults(); |
| if (mContentResolver == null || TextUtils.isEmpty(prefix)) { |
| return results; |
| } |
| |
| // Create a list to store the suggested contacts (which will be alphabetically ordered), |
| // but also keep a map of raw contact IDs to {@link SuggestedMember}s to make it easier |
| // to add supplementary data to the contact (photo, phone, email) to the members based |
| // on raw contact IDs after the second query is completed. |
| List<SuggestedMember> suggestionsList = new ArrayList<SuggestedMember>(); |
| HashMap<Long, SuggestedMember> suggestionsMap = new HashMap<Long, SuggestedMember>(); |
| |
| // First query for all the raw contacts that match the given search query |
| // and have the same account name and type as specified in this adapter |
| String searchQuery = prefix.toString() + "%"; |
| String accountClause = RawContacts.ACCOUNT_NAME + "=? AND " + |
| RawContacts.ACCOUNT_TYPE + "=?"; |
| String[] args; |
| if (mDataSet == null) { |
| accountClause += " AND " + RawContacts.DATA_SET + " IS NULL"; |
| args = new String[] {mAccountName, mAccountType, searchQuery, searchQuery}; |
| } else { |
| accountClause += " AND " + RawContacts.DATA_SET + "=?"; |
| args = new String[] { |
| mAccountName, mAccountType, mDataSet, searchQuery, searchQuery |
| }; |
| } |
| |
| Cursor cursor = mContentResolver.query( |
| RawContacts.CONTENT_URI, PROJECTION_FILTERED_MEMBERS, |
| accountClause + " AND (" + |
| RawContacts.DISPLAY_NAME_PRIMARY + " LIKE ? OR " + |
| RawContacts.DISPLAY_NAME_ALTERNATIVE + " LIKE ? )", |
| args, RawContacts.DISPLAY_NAME_PRIMARY + " COLLATE LOCALIZED ASC"); |
| |
| if (cursor == null) { |
| return results; |
| } |
| |
| // Read back the results from the cursor and filter out existing group members. |
| // For valid suggestions, add them to the hash map of suggested members. |
| try { |
| cursor.moveToPosition(-1); |
| while (cursor.moveToNext() && suggestionsMap.keySet().size() < SUGGESTIONS_LIMIT) { |
| long rawContactId = cursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX); |
| long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); |
| // Filter out contacts that have already been added to this group |
| if (mExistingMemberContactIds.contains(contactId)) { |
| continue; |
| } |
| // Otherwise, add the contact as a suggested new group member |
| String displayName = cursor.getString(DISPLAY_NAME_PRIMARY_COLUMN_INDEX); |
| SuggestedMember member = new SuggestedMember(rawContactId, displayName, |
| contactId); |
| // Store the member in the list of suggestions and add it to the hash map too. |
| suggestionsList.add(member); |
| suggestionsMap.put(rawContactId, member); |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| int numSuggestions = suggestionsMap.keySet().size(); |
| if (numSuggestions == 0) { |
| return results; |
| } |
| |
| // Create a part of the selection string for the next query with the pattern (?, ?, ?) |
| // where the number of comma-separated question marks represent the number of raw |
| // contact IDs found in the previous query (while respective the SUGGESTION_LIMIT) |
| final StringBuilder rawContactIdSelectionBuilder = new StringBuilder(); |
| final String[] questionMarks = new String[numSuggestions]; |
| Arrays.fill(questionMarks, "?"); |
| rawContactIdSelectionBuilder.append(RawContacts._ID + " IN (") |
| .append(TextUtils.join(",", questionMarks)) |
| .append(")"); |
| |
| // Construct the selection args based on the raw contact IDs we're interested in |
| // (as well as the photo, email, and phone mimetypes) |
| List<String> selectionArgs = new ArrayList<String>(); |
| selectionArgs.add(Photo.CONTENT_ITEM_TYPE); |
| selectionArgs.add(Email.CONTENT_ITEM_TYPE); |
| selectionArgs.add(Phone.CONTENT_ITEM_TYPE); |
| for (Long rawContactId : suggestionsMap.keySet()) { |
| selectionArgs.add(String.valueOf(rawContactId)); |
| } |
| |
| // Perform a second query to retrieve a photo and possibly a phone number or email |
| // address for the suggested contact |
| Cursor memberDataCursor = mContentResolver.query( |
| RawContactsEntity.CONTENT_URI, PROJECTION_MEMBER_DATA, |
| "(" + Data.MIMETYPE + "=? OR " + Data.MIMETYPE + "=? OR " + Data.MIMETYPE + |
| "=?) AND " + rawContactIdSelectionBuilder.toString(), |
| selectionArgs.toArray(new String[0]), null); |
| |
| if (memberDataCursor != null) { |
| try { |
| memberDataCursor.moveToPosition(-1); |
| while (memberDataCursor.moveToNext()) { |
| long rawContactId = memberDataCursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX); |
| SuggestedMember member = suggestionsMap.get(rawContactId); |
| if (member == null) { |
| continue; |
| } |
| String mimetype = memberDataCursor.getString(MIMETYPE_COLUMN_INDEX); |
| if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) { |
| // Set photo |
| byte[] bitmapArray = memberDataCursor.getBlob(PHOTO_COLUMN_INDEX); |
| member.setPhotoByteArray(bitmapArray); |
| } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype) || |
| Phone.CONTENT_ITEM_TYPE.equals(mimetype)) { |
| // Set at most 1 extra piece of contact info that can be a phone number or |
| // email |
| if (!member.hasExtraInfo()) { |
| String info = memberDataCursor.getString(DATA_COLUMN_INDEX); |
| member.setExtraInfo(info); |
| } |
| } |
| } |
| } finally { |
| memberDataCursor.close(); |
| } |
| } |
| results.values = suggestionsList; |
| return results; |
| } |
| |
| @Override |
| protected void publishResults(CharSequence constraint, FilterResults results) { |
| @SuppressWarnings("unchecked") |
| List<SuggestedMember> suggestionsList = (List<SuggestedMember>) results.values; |
| if (suggestionsList == null) { |
| return; |
| } |
| |
| // Clear out the existing suggestions in this adapter |
| clear(); |
| |
| // Add all the suggested members to this adapter |
| for (SuggestedMember member : suggestionsList) { |
| add(member); |
| } |
| |
| notifyDataSetChanged(); |
| } |
| } |
| |
| /** |
| * This represents a single contact that is a suggestion for the user to add to a group. |
| */ |
| // TODO: Merge this with the {@link GroupEditorFragment} Member class once we can find the |
| // lookup URI for this contact using the autocomplete filter queries |
| public class SuggestedMember { |
| |
| private long mRawContactId; |
| private long mContactId; |
| private String mDisplayName; |
| private String mExtraInfo; |
| private byte[] mPhoto; |
| |
| public SuggestedMember(long rawContactId, String displayName, long contactId) { |
| mRawContactId = rawContactId; |
| mDisplayName = displayName; |
| mContactId = contactId; |
| } |
| |
| public String getDisplayName() { |
| return mDisplayName; |
| } |
| |
| public String getExtraInfo() { |
| return mExtraInfo; |
| } |
| |
| public long getRawContactId() { |
| return mRawContactId; |
| } |
| |
| public long getContactId() { |
| return mContactId; |
| } |
| |
| public byte[] getPhotoByteArray() { |
| return mPhoto; |
| } |
| |
| public boolean hasExtraInfo() { |
| return mExtraInfo != null; |
| } |
| |
| /** |
| * Set a phone number or email to distinguish this contact |
| */ |
| public void setExtraInfo(String info) { |
| mExtraInfo = info; |
| } |
| |
| public void setPhotoByteArray(byte[] photo) { |
| mPhoto = photo; |
| } |
| |
| @Override |
| public String toString() { |
| return getDisplayName(); |
| } |
| } |
| } |