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 */
16package com.android.contacts.group;
17
18import android.content.ContentResolver;
19import android.content.Context;
20import android.database.Cursor;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.provider.ContactsContract.CommonDataKinds.Email;
24import android.provider.ContactsContract.CommonDataKinds.Phone;
25import android.provider.ContactsContract.CommonDataKinds.Photo;
26import android.provider.ContactsContract.Contacts.Data;
27import android.provider.ContactsContract.RawContacts;
28import android.provider.ContactsContract.RawContactsEntity;
29import android.text.TextUtils;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.ArrayAdapter;
34import android.widget.AutoCompleteTextView;
35import android.widget.Filter;
36import android.widget.ImageView;
37import android.widget.TextView;
38
39import com.android.contacts.R;
40import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember;
41
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.HashMap;
45import 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 */
51public 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