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