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