Home | History | Annotate | Download | only in group
      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