RecipientAlternatesAdapter.java revision 43a3fc6494a37810e047a1154e19c9e991dd5353
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
17package com.android.ex.chips;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.database.MatrixCursor;
22import android.text.util.Rfc822Token;
23import android.text.util.Rfc822Tokenizer;
24import android.util.Log;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.CursorAdapter;
29import android.widget.ImageView;
30import android.widget.TextView;
31
32import com.android.ex.chips.Queries.Query;
33
34import java.util.HashMap;
35import java.util.HashSet;
36
37/**
38 * RecipientAlternatesAdapter backs the RecipientEditTextView for managing contacts
39 * queried by email or by phone number.
40 */
41public class RecipientAlternatesAdapter extends CursorAdapter {
42    static final int MAX_LOOKUPS = 50;
43    private final LayoutInflater mLayoutInflater;
44
45    private final long mCurrentId;
46
47    private int mCheckedItemPosition = -1;
48
49    private OnCheckedItemChangedListener mCheckedItemChangedListener;
50
51    private static final String TAG = "RecipAlternates";
52
53    public static final int QUERY_TYPE_EMAIL = 0;
54    public static final int QUERY_TYPE_PHONE = 1;
55    private Query mQuery;
56
57    public static HashMap<String, RecipientEntry> getMatchingRecipients(Context context,
58            String[] inAddresses) {
59        return getMatchingRecipients(context, inAddresses, QUERY_TYPE_EMAIL);
60    }
61
62    /**
63     * Get a HashMap of address to RecipientEntry that contains all contact
64     * information for a contact with the provided address, if one exists. This
65     * may block the UI, so run it in an async task.
66     *
67     * @param context Context.
68     * @param inAddresses Array of addresses on which to perform the lookup.
69     * @return HashMap<String,RecipientEntry>
70     */
71    public static HashMap<String, RecipientEntry> getMatchingRecipients(Context context,
72            String[] inAddresses, int addressType) {
73        Queries.Query query;
74        if (addressType == QUERY_TYPE_EMAIL) {
75            query = Queries.EMAIL;
76        } else {
77            query = Queries.PHONE;
78        }
79        int addressesSize = Math.min(MAX_LOOKUPS, inAddresses.length);
80        String[] addresses = new String[addressesSize];
81        StringBuilder bindString = new StringBuilder();
82        // Create the "?" string and set up arguments.
83        for (int i = 0; i < addressesSize; i++) {
84            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses[i].toLowerCase());
85            addresses[i] = (tokens.length > 0 ? tokens[0].getAddress() : inAddresses[i]);
86            bindString.append("?");
87            if (i < addressesSize - 1) {
88                bindString.append(",");
89            }
90        }
91
92        if (Log.isLoggable(TAG, Log.DEBUG)) {
93            Log.d(TAG, "Doing reverse lookup for " + addresses.toString());
94        }
95
96        HashMap<String, RecipientEntry> recipientEntries = new HashMap<String, RecipientEntry>();
97        Cursor c = context.getContentResolver().query(
98                query.getContentUri(),
99                query.getProjection(),
100                query.getProjection()[Queries.Query.DESTINATION] + " IN (" + bindString.toString()
101                        + ")", addresses, null);
102
103        if (c != null) {
104            try {
105                if (c.moveToFirst()) {
106                    do {
107                        String address = c.getString(Queries.Query.DESTINATION);
108                        recipientEntries.put(address, RecipientEntry.constructTopLevelEntry(
109                                c.getString(Queries.Query.NAME),
110                                c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),
111                                c.getString(Queries.Query.DESTINATION),
112                                c.getInt(Queries.Query.DESTINATION_TYPE),
113                                c.getString(Queries.Query.DESTINATION_LABEL),
114                                c.getLong(Queries.Query.CONTACT_ID),
115                                c.getLong(Queries.Query.DATA_ID),
116                                c.getString(Queries.Query.PHOTO_THUMBNAIL_URI)));
117                        if (Log.isLoggable(TAG, Log.DEBUG)) {
118                            Log.d(TAG, "Received reverse look up information for " + address
119                                    + " RESULTS: "
120                                    + " NAME : " + c.getString(Queries.Query.NAME)
121                                    + " CONTACT ID : " + c.getLong(Queries.Query.CONTACT_ID)
122                                    + " ADDRESS :" + c.getString(Queries.Query.DESTINATION));
123                        }
124                    } while (c.moveToNext());
125                }
126            } finally {
127                c.close();
128            }
129        }
130        return recipientEntries;
131    }
132
133    public RecipientAlternatesAdapter(Context context, long contactId, long currentId, int viewId,
134            OnCheckedItemChangedListener listener) {
135        this(context, contactId, currentId, viewId, QUERY_TYPE_EMAIL, listener);
136    }
137
138    public RecipientAlternatesAdapter(Context context, long contactId, long currentId, int viewId,
139            int queryMode, OnCheckedItemChangedListener listener) {
140        super(context, getCursorForConstruction(context, contactId, queryMode), 0);
141        mLayoutInflater = LayoutInflater.from(context);
142        mCurrentId = currentId;
143        mCheckedItemChangedListener = listener;
144
145        if (queryMode == QUERY_TYPE_EMAIL) {
146            mQuery = Queries.EMAIL;
147        } else if (queryMode == QUERY_TYPE_PHONE) {
148            mQuery = Queries.PHONE;
149        } else {
150            mQuery = Queries.EMAIL;
151            Log.e(TAG, "Unsupported query type: " + queryMode);
152        }
153    }
154
155    private static Cursor getCursorForConstruction(Context context, long contactId, int queryType) {
156        final Cursor cursor;
157        if (queryType == QUERY_TYPE_EMAIL) {
158            cursor = context.getContentResolver().query(
159                    Queries.EMAIL.getContentUri(),
160                    Queries.EMAIL.getProjection(),
161                    Queries.EMAIL.getProjection()[Queries.Query.CONTACT_ID] + " =?", new String[] {
162                        String.valueOf(contactId)
163                    }, null);
164        } else {
165            cursor = context.getContentResolver().query(
166                    Queries.PHONE.getContentUri(),
167                    Queries.PHONE.getProjection(),
168                    Queries.PHONE.getProjection()[Queries.Query.CONTACT_ID] + " =?", new String[] {
169                        String.valueOf(contactId)
170                    }, null);
171        }
172        return removeDuplicateDestinations(cursor);
173    }
174
175    /**
176     * @return a new cursor based on the given cursor with all duplicate destinations removed.
177     *
178     * It's only intended to use for the alternate list, so...
179     * - This method ignores all other fields and dedupe solely on the destination.  Normally,
180     * if a cursor contains multiple contacts and they have the same destination, we'd still want
181     * to show both.
182     * - This method creates a MatrixCursor, so all data will be kept in memory.  We wouldn't want
183     * to do this if the original cursor is large, but it's okay here because the alternate list
184     * won't be that big.
185     */
186    // Visible for testing
187    /* package */ static Cursor removeDuplicateDestinations(Cursor original) {
188        final MatrixCursor result = new MatrixCursor(
189                original.getColumnNames(), original.getCount());
190        final HashSet<String> destinationsSeen = new HashSet<String>();
191
192        original.moveToPosition(-1);
193        while (original.moveToNext()) {
194            final String destination = original.getString(Query.DESTINATION);
195            if (destinationsSeen.contains(destination)) {
196                continue;
197            }
198            destinationsSeen.add(destination);
199
200            result.addRow(new Object[] {
201                    original.getString(Query.NAME),
202                    original.getString(Query.DESTINATION),
203                    original.getInt(Query.DESTINATION_TYPE),
204                    original.getString(Query.DESTINATION_LABEL),
205                    original.getLong(Query.CONTACT_ID),
206                    original.getLong(Query.DATA_ID),
207                    original.getString(Query.PHOTO_THUMBNAIL_URI),
208                    original.getInt(Query.DISPLAY_NAME_SOURCE)
209                    });
210        }
211
212        return result;
213    }
214
215    @Override
216    public long getItemId(int position) {
217        Cursor c = getCursor();
218        if (c.moveToPosition(position)) {
219            c.getLong(Queries.Query.DATA_ID);
220        }
221        return -1;
222    }
223
224    public RecipientEntry getRecipientEntry(int position) {
225        Cursor c = getCursor();
226        c.moveToPosition(position);
227        return RecipientEntry.constructTopLevelEntry(
228                c.getString(Queries.Query.NAME),
229                c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),
230                c.getString(Queries.Query.DESTINATION),
231                c.getInt(Queries.Query.DESTINATION_TYPE),
232                c.getString(Queries.Query.DESTINATION_LABEL),
233                c.getLong(Queries.Query.CONTACT_ID),
234                c.getLong(Queries.Query.DATA_ID),
235                c.getString(Queries.Query.PHOTO_THUMBNAIL_URI));
236    }
237
238    @Override
239    public View getView(int position, View convertView, ViewGroup parent) {
240        Cursor cursor = getCursor();
241        cursor.moveToPosition(position);
242        if (convertView == null) {
243            convertView = newView();
244        }
245        if (cursor.getLong(Queries.Query.DATA_ID) == mCurrentId) {
246            mCheckedItemPosition = position;
247            if (mCheckedItemChangedListener != null) {
248                mCheckedItemChangedListener.onCheckedItemChanged(mCheckedItemPosition);
249            }
250        }
251        bindView(convertView, convertView.getContext(), cursor);
252        return convertView;
253    }
254
255    // TODO: this is VERY similar to the BaseRecipientAdapter. Can we combine
256    // somehow?
257    @Override
258    public void bindView(View view, Context context, Cursor cursor) {
259        int position = cursor.getPosition();
260
261        TextView display = (TextView) view.findViewById(android.R.id.title);
262        ImageView imageView = (ImageView) view.findViewById(android.R.id.icon);
263        RecipientEntry entry = getRecipientEntry(position);
264        if (position == 0) {
265            display.setText(cursor.getString(Queries.Query.NAME));
266            display.setVisibility(View.VISIBLE);
267            // TODO: see if this needs to be done outside the main thread
268            // as it may be too slow to get immediately.
269            imageView.setImageURI(entry.getPhotoThumbnailUri());
270            imageView.setVisibility(View.VISIBLE);
271        } else {
272            display.setVisibility(View.GONE);
273            imageView.setVisibility(View.GONE);
274        }
275        TextView destination = (TextView) view.findViewById(android.R.id.text1);
276        destination.setText(cursor.getString(Queries.Query.DESTINATION));
277
278        TextView destinationType = (TextView) view.findViewById(android.R.id.text2);
279        if (destinationType != null) {
280            destinationType.setText(mQuery.getTypeLabel(context.getResources(),
281                    cursor.getInt(Queries.Query.DESTINATION_TYPE),
282                    cursor.getString(Queries.Query.DESTINATION_LABEL)).toString().toUpperCase());
283        }
284    }
285
286    @Override
287    public View newView(Context context, Cursor cursor, ViewGroup parent) {
288        return newView();
289    }
290
291    private View newView() {
292        return mLayoutInflater.inflate(R.layout.chips_recipient_dropdown_item, null);
293    }
294
295    /*package*/ static interface OnCheckedItemChangedListener {
296        public void onCheckedItemChanged(int position);
297    }
298}
299