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