PhoneNumberListAdapter.java revision 6af66a800ccf7599ea37374c8fc99a1489ef8fcb
1/*
2 * Copyright (C) 2010 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.common.list;
17
18import android.content.ContentUris;
19import android.content.Context;
20import android.content.CursorLoader;
21import android.database.Cursor;
22import android.net.Uri;
23import android.net.Uri.Builder;
24import android.provider.ContactsContract;
25import android.provider.ContactsContract.CommonDataKinds.Callable;
26import android.provider.ContactsContract.CommonDataKinds.Phone;
27import android.provider.ContactsContract.CommonDataKinds.SipAddress;
28import android.provider.ContactsContract.ContactCounts;
29import android.provider.ContactsContract.Contacts;
30import android.provider.ContactsContract.Data;
31import android.provider.ContactsContract.Directory;
32import android.util.Log;
33import android.view.View;
34import android.view.ViewGroup;
35
36import com.android.contacts.common.GeoUtil;
37import com.android.contacts.common.R;
38import com.android.contacts.common.extensions.ExtendedPhoneDirectoriesManager;
39import com.android.contacts.common.extensions.ExtensionsFactory;
40
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and
46 * {@link SipAddress#CONTENT_ITEM_TYPE}.
47 *
48 * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is
49 * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
50 * API instead of {@link Phone}.
51 */
52public class PhoneNumberListAdapter extends ContactEntryListAdapter {
53
54    private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
55
56    // A list of extended directories to add to the directories from the database
57    private final List<DirectoryPartition> mExtendedDirectories;
58
59    // Extended directories will have ID's that are higher than any of the id's from the database.
60    // Thi sis so that we can identify them and set them up properly. If no extended directories
61    // exist, this will be Long.MAX_VALUE
62    private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
63
64    public static class PhoneQuery {
65        public static final String[] PROJECTION_PRIMARY = new String[] {
66            Phone._ID,                          // 0
67            Phone.TYPE,                         // 1
68            Phone.LABEL,                        // 2
69            Phone.NUMBER,                       // 3
70            Phone.CONTACT_ID,                   // 4
71            Phone.LOOKUP_KEY,                   // 5
72            Phone.PHOTO_ID,                     // 6
73            Phone.DISPLAY_NAME_PRIMARY,         // 7
74            Phone.PHOTO_THUMBNAIL_URI,          // 8
75        };
76
77        public static final String[] PROJECTION_ALTERNATIVE = new String[] {
78            Phone._ID,                          // 0
79            Phone.TYPE,                         // 1
80            Phone.LABEL,                        // 2
81            Phone.NUMBER,                       // 3
82            Phone.CONTACT_ID,                   // 4
83            Phone.LOOKUP_KEY,                   // 5
84            Phone.PHOTO_ID,                     // 6
85            Phone.DISPLAY_NAME_ALTERNATIVE,     // 7
86            Phone.PHOTO_THUMBNAIL_URI,          // 8
87        };
88
89        public static final int PHONE_ID                = 0;
90        public static final int PHONE_TYPE              = 1;
91        public static final int PHONE_LABEL             = 2;
92        public static final int PHONE_NUMBER            = 3;
93        public static final int CONTACT_ID              = 4;
94        public static final int LOOKUP_KEY              = 5;
95        public static final int PHOTO_ID                = 6;
96        public static final int DISPLAY_NAME            = 7;
97        public static final int PHOTO_URI               = 8;
98    }
99
100    private final CharSequence mUnknownNameText;
101
102    private ContactListItemView.PhotoPosition mPhotoPosition;
103
104    private boolean mUseCallableUri;
105
106    public PhoneNumberListAdapter(Context context) {
107        super(context);
108        setDefaultFilterHeaderText(R.string.list_filter_phones);
109        mUnknownNameText = context.getText(android.R.string.unknownName);
110
111        final ExtendedPhoneDirectoriesManager manager
112                = ExtensionsFactory.getExtendedPhoneDirectoriesManager();
113        if (manager != null) {
114            mExtendedDirectories = manager.getExtendedDirectories(mContext);
115        } else {
116            mExtendedDirectories = null;
117        }
118    }
119
120    protected CharSequence getUnknownNameText() {
121        return mUnknownNameText;
122    }
123
124    @Override
125    public void configureLoader(CursorLoader loader, long directoryId) {
126        String query = getQueryString();
127        if (query == null) {
128            query = "";
129        }
130        if (isExtendedDirectory(directoryId)) {
131            final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
132            final String contentUri = directory.getContentUri();
133            if (contentUri == null) {
134                throw new IllegalStateException("Extended directory must have a content URL: "
135                        + directory);
136            }
137            final Builder builder = Uri.parse(contentUri).buildUpon();
138            builder.appendPath(query);
139            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
140                    String.valueOf(getDirectoryResultLimit(directory)));
141            loader.setUri(builder.build());
142            loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
143        } else {
144            final boolean isRemoteDirectoryQuery = isRemoteDirectory(directoryId);
145            final Builder builder;
146            if (isSearchMode()) {
147                final Uri baseUri;
148                if (isRemoteDirectoryQuery) {
149                    baseUri = Phone.CONTENT_FILTER_URI;
150                } else if (mUseCallableUri) {
151                    baseUri = Callable.CONTENT_FILTER_URI;
152                } else {
153                    baseUri = Phone.CONTENT_FILTER_URI;
154                }
155                builder = baseUri.buildUpon();
156                builder.appendPath(query);      // Builder will encode the query
157                builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
158                        String.valueOf(directoryId));
159                if (isRemoteDirectoryQuery) {
160                    builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
161                            String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
162                }
163            } else {
164                final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
165                builder = baseUri.buildUpon().appendQueryParameter(
166                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
167                if (isSectionHeaderDisplayEnabled()) {
168                    builder.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true");
169                }
170                applyFilter(loader, builder, directoryId, getFilter());
171            }
172
173            // Remove duplicates when it is possible.
174            builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
175            loader.setUri(builder.build());
176
177            // TODO a projection that includes the search snippet
178            if (getContactNameDisplayOrder() ==
179                    ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
180                loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
181            } else {
182                loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
183            }
184
185            if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
186                loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
187            } else {
188                loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
189            }
190        }
191    }
192
193    protected boolean isExtendedDirectory(long directoryId) {
194        return directoryId >= mFirstExtendedDirectoryId;
195    }
196
197    private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
198        final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
199        return mExtendedDirectories.get(directoryIndex);
200    }
201
202    /**
203     * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
204     * filter}.
205     */
206    private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId,
207            ContactListFilter filter) {
208        if (filter == null || directoryId != Directory.DEFAULT) {
209            return;
210        }
211
212        final StringBuilder selection = new StringBuilder();
213        final List<String> selectionArgs = new ArrayList<String>();
214
215        switch (filter.filterType) {
216            case ContactListFilter.FILTER_TYPE_CUSTOM: {
217                selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
218                selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
219                break;
220            }
221            case ContactListFilter.FILTER_TYPE_ACCOUNT: {
222                filter.addAccountQueryParameterToUrl(uriBuilder);
223                break;
224            }
225            case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
226            case ContactListFilter.FILTER_TYPE_DEFAULT:
227                break; // No selection needed.
228            case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
229                break; // This adapter is always "phone only", so no selection needed either.
230            default:
231                Log.w(TAG, "Unsupported filter type came " +
232                        "(type: " + filter.filterType + ", toString: " + filter + ")" +
233                        " showing all contacts.");
234                // No selection.
235                break;
236        }
237        loader.setSelection(selection.toString());
238        loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
239    }
240
241    @Override
242    public String getContactDisplayName(int position) {
243        return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
244    }
245
246    public String getPhoneNumber(int position) {
247        final Cursor item = (Cursor)getItem(position);
248        return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
249    }
250
251    /**
252     * Builds a {@link Data#CONTENT_URI} for the given cursor position.
253     *
254     * @return Uri for the data. may be null if the cursor is not ready.
255     */
256    public Uri getDataUri(int position) {
257        final int partitionIndex = getPartitionForPosition(position);
258        final Cursor item = (Cursor)getItem(position);
259        return item != null ? getDataUri(partitionIndex, item) : null;
260    }
261
262    public Uri getDataUri(int partitionIndex, Cursor cursor) {
263        final long directoryId =
264                ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
265        if (!isRemoteDirectory(directoryId)) {
266            final long phoneId = cursor.getLong(PhoneQuery.PHONE_ID);
267            return ContentUris.withAppendedId(Data.CONTENT_URI, phoneId);
268        }
269        return null;
270    }
271
272    @Override
273    protected View newView(Context context, int partition, Cursor cursor, int position,
274            ViewGroup parent) {
275        final ContactListItemView view = new ContactListItemView(context, null);
276        view.setUnknownNameText(mUnknownNameText);
277        view.setQuickContactEnabled(isQuickContactEnabled());
278        view.setPhotoPosition(mPhotoPosition);
279        return view;
280    }
281
282    protected void setHighlight(ContactListItemView view, Cursor cursor) {
283        view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
284    }
285
286    // Override default, which would return number of phone numbers, so we
287    // instead return number of contacts.
288    @Override
289    protected int getResultCount(Cursor cursor) {
290        if (cursor == null) {
291            return 0;
292        }
293        cursor.moveToPosition(-1);
294        long curContactId = -1;
295        int numContacts = 0;
296        while(cursor.moveToNext()) {
297            final long contactId = cursor.getLong(PhoneQuery.CONTACT_ID);
298            if (contactId != curContactId) {
299                curContactId = contactId;
300                ++numContacts;
301            }
302        }
303        return numContacts;
304    }
305
306    @Override
307    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
308        ContactListItemView view = (ContactListItemView)itemView;
309
310        setHighlight(view, cursor);
311
312        // Look at elements before and after this position, checking if contact IDs are same.
313        // If they have one same contact ID, it means they can be grouped.
314        //
315        // In one group, only the first entry will show its photo and its name, and the other
316        // entries in the group show just their data (e.g. phone number, email address).
317        cursor.moveToPosition(position);
318        boolean isFirstEntry = true;
319        boolean showBottomDivider = true;
320        final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
321        if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
322            final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
323            if (currentContactId == previousContactId) {
324                isFirstEntry = false;
325            }
326        }
327        cursor.moveToPosition(position);
328        if (cursor.moveToNext() && !cursor.isAfterLast()) {
329            final long nextContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
330            if (currentContactId == nextContactId) {
331                // The following entry should be in the same group, which means we don't want a
332                // divider between them.
333                // TODO: we want a different divider than the divider between groups. Just hiding
334                // this divider won't be enough.
335                showBottomDivider = false;
336            }
337        }
338        cursor.moveToPosition(position);
339
340        bindSectionHeaderAndDivider(view, position);
341        if (isFirstEntry) {
342            bindName(view, cursor);
343            if (isQuickContactEnabled()) {
344                bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
345                        PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID,
346                        PhoneQuery.LOOKUP_KEY);
347            } else {
348                if (getDisplayPhotos()) {
349                    bindPhoto(view, partition, cursor);
350                }
351            }
352        } else {
353            unbindName(view);
354
355            view.removePhotoView(true, false);
356        }
357
358        final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
359        bindPhoneNumber(view, cursor, directory.isDisplayNumber());
360        view.setDividerVisible(showBottomDivider);
361    }
362
363    protected void bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber) {
364        CharSequence label = null;
365        if (displayNumber &&  !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
366            final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
367            final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
368
369            // TODO cache
370            label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
371        }
372        view.setLabel(label);
373        final String text;
374        if (displayNumber) {
375            text = cursor.getString(PhoneQuery.PHONE_NUMBER);
376        } else {
377            // Display phone label. If that's null, display geocoded location for the number
378            final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
379            if (phoneLabel != null) {
380                text = phoneLabel;
381            } else {
382                final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
383                text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
384            }
385        }
386        view.setPhoneNumber(text);
387    }
388
389    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
390        if (isSectionHeaderDisplayEnabled()) {
391            Placement placement = getItemPlacementInSection(position);
392            view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
393            view.setDividerVisible(!placement.lastInSection);
394        } else {
395            view.setSectionHeader(null);
396            view.setDividerVisible(true);
397        }
398    }
399
400    protected void bindName(final ContactListItemView view, Cursor cursor) {
401        view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
402        // Note: we don't show phonetic names any more (see issue 5265330)
403    }
404
405    protected void unbindName(final ContactListItemView view) {
406        view.hideDisplayName();
407    }
408
409    protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
410        if (!isPhotoSupported(partitionIndex)) {
411            view.removePhotoView();
412            return;
413        }
414
415        long photoId = 0;
416        if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
417            photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
418        }
419
420        if (photoId != 0) {
421            getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
422        } else {
423            final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
424            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
425            getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false);
426        }
427    }
428
429    public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
430        mPhotoPosition = photoPosition;
431    }
432
433    public ContactListItemView.PhotoPosition getPhotoPosition() {
434        return mPhotoPosition;
435    }
436
437    public void setUseCallableUri(boolean useCallableUri) {
438        mUseCallableUri = useCallableUri;
439    }
440
441    public boolean usesCallableUri() {
442        return mUseCallableUri;
443    }
444
445    /**
446     * Override base implementation to inject extended directories between local & remote
447     * directories. This is done in the following steps:
448     * 1. Call base implementation to add directories from the cursor.
449     * 2. Iterate all base directories and establish the following information:
450     *   a. The highest directory id so that we can assign unused id's to the extended directories.
451     *   b. The index of the last non-remote directory. This is where we will insert extended
452     *      directories.
453     * 3. Iterate the extended directories and for each one, assign an ID and insert it in the
454     *    proper location.
455     */
456    @Override
457    public void changeDirectories(Cursor cursor) {
458        super.changeDirectories(cursor);
459        if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
460            return;
461        }
462        final int numExtendedDirectories = mExtendedDirectories.size();
463        if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
464            // already added all directories;
465            return;
466        }
467        //
468        mFirstExtendedDirectoryId = Long.MAX_VALUE;
469        if (numExtendedDirectories > 0) {
470            // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
471            // "special" ID.
472            long maxId = Directory.LOCAL_INVISIBLE;
473            int insertIndex = 0;
474            for (int i = 0, n = getPartitionCount(); i < n; i++) {
475                final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
476                final long id = partition.getDirectoryId();
477                if (id > maxId) {
478                    maxId = id;
479                }
480                if (!isRemoteDirectory(id)) {
481                    // assuming remote directories come after local, we will end up with the index
482                    // where we should insert extended directories. This also works if there are no
483                    // remote directories at all.
484                    insertIndex = i + 1;
485                }
486            }
487            // Extended directories ID's cannot collide with base directories
488            mFirstExtendedDirectoryId = maxId + 1;
489            for (int i = 0; i < numExtendedDirectories; i++) {
490                final long id = mFirstExtendedDirectoryId + i;
491                final DirectoryPartition directory = mExtendedDirectories.get(i);
492                if (getPartitionByDirectoryId(id) == -1) {
493                    addPartition(insertIndex, directory);
494                    directory.setDirectoryId(id);
495                }
496            }
497        }
498    }
499}
500