1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.contacts.group; 17 18 import android.content.ContentResolver; 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.graphics.Bitmap; 22 import android.graphics.BitmapFactory; 23 import android.provider.ContactsContract.CommonDataKinds.Email; 24 import android.provider.ContactsContract.CommonDataKinds.Phone; 25 import android.provider.ContactsContract.CommonDataKinds.Photo; 26 import android.provider.ContactsContract.Contacts.Data; 27 import android.provider.ContactsContract.RawContacts; 28 import android.provider.ContactsContract.RawContactsEntity; 29 import android.text.TextUtils; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.ArrayAdapter; 34 import android.widget.AutoCompleteTextView; 35 import android.widget.Filter; 36 import android.widget.ImageView; 37 import android.widget.TextView; 38 39 import com.android.contacts.R; 40 import com.android.contacts.common.ContactPhotoManager; 41 import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember; 42 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.HashMap; 46 import java.util.List; 47 48 /** 49 * This adapter provides suggested contacts that can be added to a group for an 50 * {@link AutoCompleteTextView} within the group editor. 51 */ 52 public class SuggestedMemberListAdapter extends ArrayAdapter<SuggestedMember> { 53 54 private static final String[] PROJECTION_FILTERED_MEMBERS = new String[] { 55 RawContacts._ID, // 0 56 RawContacts.CONTACT_ID, // 1 57 RawContacts.DISPLAY_NAME_PRIMARY // 2 58 }; 59 60 private static final int RAW_CONTACT_ID_COLUMN_INDEX = 0; 61 private static final int CONTACT_ID_COLUMN_INDEX = 1; 62 private static final int DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 2; 63 64 private static final String[] PROJECTION_MEMBER_DATA = new String[] { 65 RawContacts._ID, // 0 66 RawContacts.CONTACT_ID, // 1 67 Data.MIMETYPE, // 2 68 Data.DATA1, // 3 69 Photo.PHOTO, // 4 70 }; 71 72 private static final int MIMETYPE_COLUMN_INDEX = 2; 73 private static final int DATA_COLUMN_INDEX = 3; 74 private static final int PHOTO_COLUMN_INDEX = 4; 75 76 private Filter mFilter; 77 private ContentResolver mContentResolver; 78 private LayoutInflater mInflater; 79 80 private String mAccountType; 81 private String mAccountName; 82 private String mDataSet; 83 84 // TODO: Make this a Map for better performance when we check if a new contact is in the list 85 // or not 86 private final List<Long> mExistingMemberContactIds = new ArrayList<Long>(); 87 88 private static final int SUGGESTIONS_LIMIT = 5; 89 SuggestedMemberListAdapter(Context context, int textViewResourceId)90 public SuggestedMemberListAdapter(Context context, int textViewResourceId) { 91 super(context, textViewResourceId); 92 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 93 } 94 setAccountType(String accountType)95 public void setAccountType(String accountType) { 96 mAccountType = accountType; 97 } 98 setAccountName(String accountName)99 public void setAccountName(String accountName) { 100 mAccountName = accountName; 101 } 102 setDataSet(String dataSet)103 public void setDataSet(String dataSet) { 104 mDataSet = dataSet; 105 } 106 setContentResolver(ContentResolver resolver)107 public void setContentResolver(ContentResolver resolver) { 108 mContentResolver = resolver; 109 } 110 updateExistingMembersList(List<GroupEditorFragment.Member> list)111 public void updateExistingMembersList(List<GroupEditorFragment.Member> list) { 112 mExistingMemberContactIds.clear(); 113 for (GroupEditorFragment.Member member : list) { 114 mExistingMemberContactIds.add(member.getContactId()); 115 } 116 } 117 addNewMember(long contactId)118 public void addNewMember(long contactId) { 119 mExistingMemberContactIds.add(contactId); 120 } 121 removeMember(long contactId)122 public void removeMember(long contactId) { 123 if (mExistingMemberContactIds.contains(contactId)) { 124 mExistingMemberContactIds.remove(contactId); 125 } 126 } 127 128 @Override getView(int position, View convertView, ViewGroup parent)129 public View getView(int position, View convertView, ViewGroup parent) { 130 View result = convertView; 131 if (result == null) { 132 result = mInflater.inflate(R.layout.group_member_suggestion, parent, false); 133 } 134 // TODO: Use a viewholder 135 SuggestedMember member = getItem(position); 136 TextView text1 = (TextView) result.findViewById(R.id.text1); 137 TextView text2 = (TextView) result.findViewById(R.id.text2); 138 ImageView icon = (ImageView) result.findViewById(R.id.icon); 139 text1.setText(member.getDisplayName()); 140 if (member.hasExtraInfo()) { 141 text2.setText(member.getExtraInfo()); 142 } else { 143 text2.setVisibility(View.GONE); 144 } 145 byte[] byteArray = member.getPhotoByteArray(); 146 if (byteArray == null) { 147 icon.setImageDrawable(ContactPhotoManager.getDefaultAvatarDrawableForContact( 148 icon.getResources(), false, null)); 149 } else { 150 Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); 151 icon.setImageBitmap(bitmap); 152 } 153 result.setTag(member); 154 return result; 155 } 156 157 @Override getFilter()158 public Filter getFilter() { 159 if (mFilter == null) { 160 mFilter = new SuggestedMemberFilter(); 161 } 162 return mFilter; 163 } 164 165 /** 166 * This filter queries for raw contacts that match the given account name and account type, 167 * as well as the search query. 168 */ 169 public class SuggestedMemberFilter extends Filter { 170 171 @Override performFiltering(CharSequence prefix)172 protected FilterResults performFiltering(CharSequence prefix) { 173 FilterResults results = new FilterResults(); 174 if (mContentResolver == null || TextUtils.isEmpty(prefix)) { 175 return results; 176 } 177 178 // Create a list to store the suggested contacts (which will be alphabetically ordered), 179 // but also keep a map of raw contact IDs to {@link SuggestedMember}s to make it easier 180 // to add supplementary data to the contact (photo, phone, email) to the members based 181 // on raw contact IDs after the second query is completed. 182 List<SuggestedMember> suggestionsList = new ArrayList<SuggestedMember>(); 183 HashMap<Long, SuggestedMember> suggestionsMap = new HashMap<Long, SuggestedMember>(); 184 185 // First query for all the raw contacts that match the given search query 186 // and have the same account name and type as specified in this adapter 187 String searchQuery = prefix.toString() + "%"; 188 String accountClause = RawContacts.ACCOUNT_NAME + "=? AND " + 189 RawContacts.ACCOUNT_TYPE + "=?"; 190 String[] args; 191 if (mDataSet == null) { 192 accountClause += " AND " + RawContacts.DATA_SET + " IS NULL"; 193 args = new String[] {mAccountName, mAccountType, searchQuery, searchQuery}; 194 } else { 195 accountClause += " AND " + RawContacts.DATA_SET + "=?"; 196 args = new String[] { 197 mAccountName, mAccountType, mDataSet, searchQuery, searchQuery 198 }; 199 } 200 201 Cursor cursor = mContentResolver.query( 202 RawContacts.CONTENT_URI, PROJECTION_FILTERED_MEMBERS, 203 accountClause + " AND (" + 204 RawContacts.DISPLAY_NAME_PRIMARY + " LIKE ? OR " + 205 RawContacts.DISPLAY_NAME_ALTERNATIVE + " LIKE ? )", 206 args, RawContacts.DISPLAY_NAME_PRIMARY + " COLLATE LOCALIZED ASC"); 207 208 if (cursor == null) { 209 return results; 210 } 211 212 // Read back the results from the cursor and filter out existing group members. 213 // For valid suggestions, add them to the hash map of suggested members. 214 try { 215 cursor.moveToPosition(-1); 216 while (cursor.moveToNext() && suggestionsMap.keySet().size() < SUGGESTIONS_LIMIT) { 217 long rawContactId = cursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX); 218 long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); 219 // Filter out contacts that have already been added to this group 220 if (mExistingMemberContactIds.contains(contactId)) { 221 continue; 222 } 223 // Otherwise, add the contact as a suggested new group member 224 String displayName = cursor.getString(DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 225 SuggestedMember member = new SuggestedMember(rawContactId, displayName, 226 contactId); 227 // Store the member in the list of suggestions and add it to the hash map too. 228 suggestionsList.add(member); 229 suggestionsMap.put(rawContactId, member); 230 } 231 } finally { 232 cursor.close(); 233 } 234 235 int numSuggestions = suggestionsMap.keySet().size(); 236 if (numSuggestions == 0) { 237 return results; 238 } 239 240 // Create a part of the selection string for the next query with the pattern (?, ?, ?) 241 // where the number of comma-separated question marks represent the number of raw 242 // contact IDs found in the previous query (while respective the SUGGESTION_LIMIT) 243 final StringBuilder rawContactIdSelectionBuilder = new StringBuilder(); 244 final String[] questionMarks = new String[numSuggestions]; 245 Arrays.fill(questionMarks, "?"); 246 rawContactIdSelectionBuilder.append(RawContacts._ID + " IN (") 247 .append(TextUtils.join(",", questionMarks)) 248 .append(")"); 249 250 // Construct the selection args based on the raw contact IDs we're interested in 251 // (as well as the photo, email, and phone mimetypes) 252 List<String> selectionArgs = new ArrayList<String>(); 253 selectionArgs.add(Photo.CONTENT_ITEM_TYPE); 254 selectionArgs.add(Email.CONTENT_ITEM_TYPE); 255 selectionArgs.add(Phone.CONTENT_ITEM_TYPE); 256 for (Long rawContactId : suggestionsMap.keySet()) { 257 selectionArgs.add(String.valueOf(rawContactId)); 258 } 259 260 // Perform a second query to retrieve a photo and possibly a phone number or email 261 // address for the suggested contact 262 Cursor memberDataCursor = mContentResolver.query( 263 RawContactsEntity.CONTENT_URI, PROJECTION_MEMBER_DATA, 264 "(" + Data.MIMETYPE + "=? OR " + Data.MIMETYPE + "=? OR " + Data.MIMETYPE + 265 "=?) AND " + rawContactIdSelectionBuilder.toString(), 266 selectionArgs.toArray(new String[0]), null); 267 268 if (memberDataCursor != null) { 269 try { 270 memberDataCursor.moveToPosition(-1); 271 while (memberDataCursor.moveToNext()) { 272 long rawContactId = memberDataCursor.getLong(RAW_CONTACT_ID_COLUMN_INDEX); 273 SuggestedMember member = suggestionsMap.get(rawContactId); 274 if (member == null) { 275 continue; 276 } 277 String mimetype = memberDataCursor.getString(MIMETYPE_COLUMN_INDEX); 278 if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) { 279 // Set photo 280 byte[] bitmapArray = memberDataCursor.getBlob(PHOTO_COLUMN_INDEX); 281 member.setPhotoByteArray(bitmapArray); 282 } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype) || 283 Phone.CONTENT_ITEM_TYPE.equals(mimetype)) { 284 // Set at most 1 extra piece of contact info that can be a phone number or 285 // email 286 if (!member.hasExtraInfo()) { 287 String info = memberDataCursor.getString(DATA_COLUMN_INDEX); 288 member.setExtraInfo(info); 289 } 290 } 291 } 292 } finally { 293 memberDataCursor.close(); 294 } 295 } 296 results.values = suggestionsList; 297 return results; 298 } 299 300 @Override publishResults(CharSequence constraint, FilterResults results)301 protected void publishResults(CharSequence constraint, FilterResults results) { 302 @SuppressWarnings("unchecked") 303 List<SuggestedMember> suggestionsList = (List<SuggestedMember>) results.values; 304 if (suggestionsList == null) { 305 return; 306 } 307 308 // Clear out the existing suggestions in this adapter 309 clear(); 310 311 // Add all the suggested members to this adapter 312 for (SuggestedMember member : suggestionsList) { 313 add(member); 314 } 315 316 notifyDataSetChanged(); 317 } 318 } 319 320 /** 321 * This represents a single contact that is a suggestion for the user to add to a group. 322 */ 323 // TODO: Merge this with the {@link GroupEditorFragment} Member class once we can find the 324 // lookup URI for this contact using the autocomplete filter queries 325 public class SuggestedMember { 326 327 private long mRawContactId; 328 private long mContactId; 329 private String mDisplayName; 330 private String mExtraInfo; 331 private byte[] mPhoto; 332 SuggestedMember(long rawContactId, String displayName, long contactId)333 public SuggestedMember(long rawContactId, String displayName, long contactId) { 334 mRawContactId = rawContactId; 335 mDisplayName = displayName; 336 mContactId = contactId; 337 } 338 getDisplayName()339 public String getDisplayName() { 340 return mDisplayName; 341 } 342 getExtraInfo()343 public String getExtraInfo() { 344 return mExtraInfo; 345 } 346 getRawContactId()347 public long getRawContactId() { 348 return mRawContactId; 349 } 350 getContactId()351 public long getContactId() { 352 return mContactId; 353 } 354 getPhotoByteArray()355 public byte[] getPhotoByteArray() { 356 return mPhoto; 357 } 358 hasExtraInfo()359 public boolean hasExtraInfo() { 360 return mExtraInfo != null; 361 } 362 363 /** 364 * Set a phone number or email to distinguish this contact 365 */ setExtraInfo(String info)366 public void setExtraInfo(String info) { 367 mExtraInfo = info; 368 } 369 setPhotoByteArray(byte[] photo)370 public void setPhotoByteArray(byte[] photo) { 371 mPhoto = photo; 372 } 373 374 @Override toString()375 public String toString() { 376 return getDisplayName(); 377 } 378 } 379 } 380