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 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 
     90     public SuggestedMemberListAdapter(Context context, int textViewResourceId) {
     91         super(context, textViewResourceId);
     92         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     93     }
     94 
     95     public void setAccountType(String accountType) {
     96         mAccountType = accountType;
     97     }
     98 
     99     public void setAccountName(String accountName) {
    100         mAccountName = accountName;
    101     }
    102 
    103     public void setDataSet(String dataSet) {
    104         mDataSet = dataSet;
    105     }
    106 
    107     public void setContentResolver(ContentResolver resolver) {
    108         mContentResolver = resolver;
    109     }
    110 
    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 
    118     public void addNewMember(long contactId) {
    119         mExistingMemberContactIds.add(contactId);
    120     }
    121 
    122     public void removeMember(long contactId) {
    123         if (mExistingMemberContactIds.contains(contactId)) {
    124             mExistingMemberContactIds.remove(contactId);
    125         }
    126     }
    127 
    128     @Override
    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
    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
    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
    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 
    333         public SuggestedMember(long rawContactId, String displayName, long contactId) {
    334             mRawContactId = rawContactId;
    335             mDisplayName = displayName;
    336             mContactId = contactId;
    337         }
    338 
    339         public String getDisplayName() {
    340             return mDisplayName;
    341         }
    342 
    343         public String getExtraInfo() {
    344             return mExtraInfo;
    345         }
    346 
    347         public long getRawContactId() {
    348             return mRawContactId;
    349         }
    350 
    351         public long getContactId() {
    352             return mContactId;
    353         }
    354 
    355         public byte[] getPhotoByteArray() {
    356             return mPhoto;
    357         }
    358 
    359         public boolean hasExtraInfo() {
    360             return mExtraInfo != null;
    361         }
    362 
    363         /**
    364          * Set a phone number or email to distinguish this contact
    365          */
    366         public void setExtraInfo(String info) {
    367             mExtraInfo = info;
    368         }
    369 
    370         public void setPhotoByteArray(byte[] photo) {
    371             mPhoto = photo;
    372         }
    373 
    374         @Override
    375         public String toString() {
    376             return getDisplayName();
    377         }
    378     }
    379 }
    380