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.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.Contacts;
29import android.provider.ContactsContract.Data;
30import android.provider.ContactsContract.Directory;
31import android.text.TextUtils;
32import android.util.Log;
33import android.view.View;
34import android.view.ViewGroup;
35
36import com.android.contacts.CallUtil;
37import com.android.contacts.ContactPhotoManager.DefaultImageRequest;
38import com.android.contacts.ContactsUtils;
39import com.android.contacts.GeoUtil;
40import com.android.contacts.R;
41import com.android.contacts.compat.CallableCompat;
42import com.android.contacts.compat.CompatUtils;
43import com.android.contacts.compat.DirectoryCompat;
44import com.android.contacts.compat.PhoneCompat;
45import com.android.contacts.extensions.ExtendedPhoneDirectoriesManager;
46import com.android.contacts.extensions.ExtensionsFactory;
47import com.android.contacts.preference.ContactsPreferences;
48import com.android.contacts.util.Constants;
49
50import com.google.common.collect.Lists;
51
52import java.util.ArrayList;
53import java.util.List;
54
55/**
56 * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and
57 * {@link SipAddress#CONTENT_ITEM_TYPE}.
58 *
59 * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is
60 * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
61 * API instead of {@link Phone}.
62 */
63public class PhoneNumberListAdapter extends ContactEntryListAdapter {
64
65    private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
66
67    public interface Listener {
68        void onVideoCallIconClicked(int position);
69    }
70
71    // A list of extended directories to add to the directories from the database
72    private final List<DirectoryPartition> mExtendedDirectories;
73
74    // Extended directories will have ID's that are higher than any of the id's from the database,
75    // so that we can identify them and set them up properly. If no extended directories
76    // exist, this will be Long.MAX_VALUE
77    private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
78
79    public static class PhoneQuery {
80
81        /**
82         * Optional key used as part of a JSON lookup key to specify an analytics category
83         * associated with the row.
84         */
85        public static final String ANALYTICS_CATEGORY = "analytics_category";
86
87        /**
88         * Optional key used as part of a JSON lookup key to specify an analytics action associated
89         * with the row.
90         */
91        public static final String ANALYTICS_ACTION = "analytics_action";
92
93        /**
94         * Optional key used as part of a JSON lookup key to specify an analytics value associated
95         * with the row.
96         */
97        public static final String ANALYTICS_VALUE = "analytics_value";
98
99        public static final String[] PROJECTION_PRIMARY_INTERNAL = new String[] {
100            Phone._ID,                          // 0
101            Phone.TYPE,                         // 1
102            Phone.LABEL,                        // 2
103            Phone.NUMBER,                       // 3
104            Phone.CONTACT_ID,                   // 4
105            Phone.LOOKUP_KEY,                   // 5
106            Phone.PHOTO_ID,                     // 6
107            Phone.DISPLAY_NAME_PRIMARY,         // 7
108            Phone.PHOTO_THUMBNAIL_URI,          // 8
109        };
110
111        public static final String[] PROJECTION_PRIMARY;
112
113        static {
114            final List<String> projectionList = Lists.newArrayList(PROJECTION_PRIMARY_INTERNAL);
115            if (CompatUtils.isMarshmallowCompatible()) {
116                projectionList.add(Phone.CARRIER_PRESENCE); // 9
117            }
118            PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]);
119        }
120
121        public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = new String[] {
122            Phone._ID,                          // 0
123            Phone.TYPE,                         // 1
124            Phone.LABEL,                        // 2
125            Phone.NUMBER,                       // 3
126            Phone.CONTACT_ID,                   // 4
127            Phone.LOOKUP_KEY,                   // 5
128            Phone.PHOTO_ID,                     // 6
129            Phone.DISPLAY_NAME_ALTERNATIVE,     // 7
130            Phone.PHOTO_THUMBNAIL_URI,          // 8
131        };
132
133        public static final String[] PROJECTION_ALTERNATIVE;
134
135        static {
136            final List<String> projectionList = Lists.newArrayList(PROJECTION_ALTERNATIVE_INTERNAL);
137            if (CompatUtils.isMarshmallowCompatible()) {
138                projectionList.add(Phone.CARRIER_PRESENCE); // 9
139            }
140            PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]);
141        }
142
143        public static final int PHONE_ID                = 0;
144        public static final int PHONE_TYPE              = 1;
145        public static final int PHONE_LABEL             = 2;
146        public static final int PHONE_NUMBER            = 3;
147        public static final int CONTACT_ID              = 4;
148        public static final int LOOKUP_KEY              = 5;
149        public static final int PHOTO_ID                = 6;
150        public static final int DISPLAY_NAME            = 7;
151        public static final int PHOTO_URI               = 8;
152        public static final int CARRIER_PRESENCE        = 9;
153    }
154
155    private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE =
156            "length(" + Phone.NUMBER + ") < 1000";
157
158    private final CharSequence mUnknownNameText;
159    private final String mCountryIso;
160
161    private ContactListItemView.PhotoPosition mPhotoPosition;
162
163    private boolean mUseCallableUri;
164
165    private Listener mListener;
166
167    private boolean mIsVideoEnabled;
168    private boolean mIsPresenceEnabled;
169
170    public PhoneNumberListAdapter(Context context) {
171        super(context);
172        setDefaultFilterHeaderText(R.string.list_filter_phones);
173        mUnknownNameText = context.getText(android.R.string.unknownName);
174        mCountryIso = GeoUtil.getCurrentCountryIso(context);
175
176        final ExtendedPhoneDirectoriesManager manager
177                = ExtensionsFactory.getExtendedPhoneDirectoriesManager();
178        if (manager != null) {
179            mExtendedDirectories = manager.getExtendedDirectories(mContext);
180        } else {
181            // Empty list to avoid sticky NPE's
182            mExtendedDirectories = new ArrayList<DirectoryPartition>();
183        }
184
185        int videoCapabilities = CallUtil.getVideoCallingAvailability(context);
186        mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0;
187        mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
188    }
189
190    protected CharSequence getUnknownNameText() {
191        return mUnknownNameText;
192    }
193
194    @Override
195    public void configureLoader(CursorLoader loader, long directoryId) {
196        String query = getQueryString();
197        if (query == null) {
198            query = "";
199        }
200        if (isExtendedDirectory(directoryId)) {
201            final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
202            final String contentUri = directory.getContentUri();
203            if (contentUri == null) {
204                throw new IllegalStateException("Extended directory must have a content URL: "
205                        + directory);
206            }
207            final Builder builder = Uri.parse(contentUri).buildUpon();
208            builder.appendPath(query);
209            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
210                    String.valueOf(getDirectoryResultLimit(directory)));
211            loader.setUri(builder.build());
212            loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
213        } else {
214            final boolean isRemoteDirectoryQuery
215                    = DirectoryCompat.isRemoteDirectoryId(directoryId);
216            final Builder builder;
217            if (isSearchMode()) {
218                final Uri baseUri;
219                if (isRemoteDirectoryQuery) {
220                    baseUri = PhoneCompat.getContentFilterUri();
221                } else if (mUseCallableUri) {
222                    baseUri = CallableCompat.getContentFilterUri();
223                } else {
224                    baseUri = PhoneCompat.getContentFilterUri();
225                }
226                builder = baseUri.buildUpon();
227                builder.appendPath(query);      // Builder will encode the query
228                builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
229                        String.valueOf(directoryId));
230                if (isRemoteDirectoryQuery) {
231                    builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
232                            String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
233                }
234            } else {
235                Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
236                builder = baseUri.buildUpon().appendQueryParameter(
237                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
238                if (isSectionHeaderDisplayEnabled()) {
239                    builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true");
240                }
241                applyFilter(loader, builder, directoryId, getFilter());
242            }
243
244            // Ignore invalid phone numbers that are too long. These can potentially cause freezes
245            // in the UI and there is no reason to display them.
246            final String prevSelection = loader.getSelection();
247            final String newSelection;
248            if (!TextUtils.isEmpty(prevSelection)) {
249                newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE;
250            } else {
251                newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE;
252            }
253            loader.setSelection(newSelection);
254
255            // Remove duplicates when it is possible.
256            builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
257            loader.setUri(builder.build());
258
259            // TODO a projection that includes the search snippet
260            if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
261                loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
262            } else {
263                loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
264            }
265
266            if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
267                loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
268            } else {
269                loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
270            }
271        }
272    }
273
274    protected boolean isExtendedDirectory(long directoryId) {
275        return directoryId >= mFirstExtendedDirectoryId;
276    }
277
278    private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
279        final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
280        return mExtendedDirectories.get(directoryIndex);
281    }
282
283    /**
284     * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
285     * filter}.
286     */
287    private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId,
288            ContactListFilter filter) {
289        if (filter == null || directoryId != Directory.DEFAULT) {
290            return;
291        }
292
293        final StringBuilder selection = new StringBuilder();
294        final List<String> selectionArgs = new ArrayList<String>();
295
296        switch (filter.filterType) {
297            case ContactListFilter.FILTER_TYPE_CUSTOM: {
298                selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
299                selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
300                break;
301            }
302            case ContactListFilter.FILTER_TYPE_ACCOUNT: {
303                filter.addAccountQueryParameterToUrl(uriBuilder);
304                break;
305            }
306            case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
307            case ContactListFilter.FILTER_TYPE_DEFAULT:
308                break; // No selection needed.
309            case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
310                break; // This adapter is always "phone only", so no selection needed either.
311            default:
312                Log.w(TAG, "Unsupported filter type came " +
313                        "(type: " + filter.filterType + ", toString: " + filter + ")" +
314                        " showing all contacts.");
315                // No selection.
316                break;
317        }
318        loader.setSelection(selection.toString());
319        loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
320    }
321
322    @Override
323    public String getContactDisplayName(int position) {
324        return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
325    }
326
327    public String getPhoneNumber(int position) {
328        final Cursor item = (Cursor)getItem(position);
329        return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
330    }
331
332    /**
333     * Builds a {@link Data#CONTENT_URI} for the given cursor position.
334     *
335     * @return Uri for the data. may be null if the cursor is not ready.
336     */
337    public Uri getDataUri(int position) {
338        final int partitionIndex = getPartitionForPosition(position);
339        final Cursor item = (Cursor)getItem(position);
340        return item != null ? getDataUri(partitionIndex, item) : null;
341    }
342
343    public Uri getDataUri(int partitionIndex, Cursor cursor) {
344        final long directoryId =
345                ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
346        if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
347            return null;
348        } else if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
349            /*
350             * ContentUris.withAppendedId(Data.CONTENT_URI, phoneId), is invalid if
351             * isEnterpriseDirectoryId returns true, because the uri itself will fail since the
352             * ContactsProvider in Android Framework currently doesn't support it. return null until
353             * Android framework has enterprise version of Data.CONTENT_URI
354             */
355            return null;
356        } else {
357            final long phoneId = cursor.getLong(PhoneQuery.PHONE_ID);
358            return ContentUris.withAppendedId(Data.CONTENT_URI, phoneId);
359        }
360    }
361
362    /**
363     * Retrieves the lookup key for the given cursor position.
364     *
365     * @param position The cursor position.
366     * @return The lookup key.
367     */
368    public String getLookupKey(int position) {
369        final Cursor item = (Cursor)getItem(position);
370        return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null;
371    }
372
373    @Override
374    protected ContactListItemView newView(
375            Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
376        ContactListItemView view = super.newView(context, partition, cursor, position, parent);
377        view.setUnknownNameText(mUnknownNameText);
378        view.setQuickContactEnabled(isQuickContactEnabled());
379        view.setPhotoPosition(mPhotoPosition);
380        return view;
381    }
382
383    protected void setHighlight(ContactListItemView view, Cursor cursor) {
384        view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
385    }
386
387    // Override default, which would return number of phone numbers, so we
388    // instead return number of contacts.
389    @Override
390    protected int getResultCount(Cursor cursor) {
391        if (cursor == null) {
392            return 0;
393        }
394        cursor.moveToPosition(-1);
395        long curContactId = -1;
396        int numContacts = 0;
397        while(cursor.moveToNext()) {
398            final long contactId = cursor.getLong(PhoneQuery.CONTACT_ID);
399            if (contactId != curContactId) {
400                curContactId = contactId;
401                ++numContacts;
402            }
403        }
404        return numContacts;
405    }
406
407    @Override
408    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
409        super.bindView(itemView, partition, cursor, position);
410        ContactListItemView view = (ContactListItemView)itemView;
411
412        setHighlight(view, cursor);
413
414        // Look at elements before and after this position, checking if contact IDs are same.
415        // If they have one same contact ID, it means they can be grouped.
416        //
417        // In one group, only the first entry will show its photo and its name, and the other
418        // entries in the group show just their data (e.g. phone number, email address).
419        cursor.moveToPosition(position);
420        boolean isFirstEntry = true;
421        boolean showBottomDivider = true;
422        final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
423        if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
424            final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
425            if (currentContactId == previousContactId) {
426                isFirstEntry = false;
427            }
428        }
429        cursor.moveToPosition(position);
430        if (cursor.moveToNext() && !cursor.isAfterLast()) {
431            final long nextContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
432            if (currentContactId == nextContactId) {
433                // The following entry should be in the same group, which means we don't want a
434                // divider between them.
435                // TODO: we want a different divider than the divider between groups. Just hiding
436                // this divider won't be enough.
437                showBottomDivider = false;
438            }
439        }
440        cursor.moveToPosition(position);
441
442        bindViewId(view, cursor, PhoneQuery.PHONE_ID);
443
444        bindSectionHeaderAndDivider(view, position);
445        if (isFirstEntry) {
446            bindName(view, cursor);
447            if (isQuickContactEnabled()) {
448                bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
449                        PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID,
450                        PhoneQuery.LOOKUP_KEY, PhoneQuery.DISPLAY_NAME);
451            } else {
452                if (getDisplayPhotos()) {
453                    bindPhoto(view, partition, cursor);
454                }
455            }
456        } else {
457            unbindName(view);
458
459            view.removePhotoView(true, false);
460        }
461
462        final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
463        bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position);
464    }
465
466    protected void bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber,
467            int position) {
468        CharSequence label = null;
469        if (displayNumber &&  !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
470            final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
471            final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
472
473            // TODO cache
474            label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
475        }
476        view.setLabel(label);
477        final String text;
478        if (displayNumber) {
479            text = cursor.getString(PhoneQuery.PHONE_NUMBER);
480        } else {
481            // Display phone label. If that's null, display geocoded location for the number
482            final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
483            if (phoneLabel != null) {
484                text = phoneLabel;
485            } else {
486                final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
487                text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
488            }
489        }
490        view.setPhoneNumber(text, mCountryIso);
491
492        if (CompatUtils.isVideoCompatible()) {
493            // Determine if carrier presence indicates the number supports video calling.
494            int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE);
495            boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
496
497            boolean isVideoIconShown = mIsVideoEnabled && (
498                    mIsPresenceEnabled && isPresent || !mIsPresenceEnabled);
499            view.setShowVideoCallIcon(isVideoIconShown, mListener, position);
500        }
501    }
502
503    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
504        if (isSectionHeaderDisplayEnabled()) {
505            Placement placement = getItemPlacementInSection(position);
506            view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
507        } else {
508            view.setSectionHeader(null);
509        }
510    }
511
512    protected void bindName(final ContactListItemView view, Cursor cursor) {
513        view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
514        // Note: we don't show phonetic names any more (see issue 5265330)
515    }
516
517    protected void unbindName(final ContactListItemView view) {
518        view.hideDisplayName();
519    }
520
521    @Override
522    protected void bindWorkProfileIcon(final ContactListItemView view, int partition) {
523        final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
524        final long directoryId = directory.getDirectoryId();
525        final long userType = ContactsUtils.determineUserType(directoryId, null);
526        // Work directory must not be a extended directory. An extended directory is custom
527        // directory in the app, but not a directory provided by framework. So it can't be
528        // USER_TYPE_WORK.
529        view.setWorkProfileIconEnabled(
530                !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK);
531    }
532
533    protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
534        if (!isPhotoSupported(partitionIndex)) {
535            view.removePhotoView();
536            return;
537        }
538
539        long photoId = 0;
540        if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
541            photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
542        }
543
544        if (photoId != 0) {
545            getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false,
546                    getCircularPhotos(), null);
547        } else {
548            final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
549            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
550
551            DefaultImageRequest request = null;
552            if (photoUri == null) {
553                final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME);
554                final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY);
555                request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos());
556            }
557            getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
558                    getCircularPhotos(), request);
559        }
560    }
561
562    public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
563        mPhotoPosition = photoPosition;
564    }
565
566    public ContactListItemView.PhotoPosition getPhotoPosition() {
567        return mPhotoPosition;
568    }
569
570    public void setUseCallableUri(boolean useCallableUri) {
571        mUseCallableUri = useCallableUri;
572    }
573
574    public boolean usesCallableUri() {
575        return mUseCallableUri;
576    }
577
578    /**
579     * Override base implementation to inject extended directories between local & remote
580     * directories. This is done in the following steps:
581     * 1. Call base implementation to add directories from the cursor.
582     * 2. Iterate all base directories and establish the following information:
583     *   a. The highest directory id so that we can assign unused id's to the extended directories.
584     *   b. The index of the last non-remote directory. This is where we will insert extended
585     *      directories.
586     * 3. Iterate the extended directories and for each one, assign an ID and insert it in the
587     *    proper location.
588     */
589    @Override
590    public void changeDirectories(Cursor cursor) {
591        super.changeDirectories(cursor);
592        if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
593            return;
594        }
595        final int numExtendedDirectories = mExtendedDirectories.size();
596        if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
597            // already added all directories;
598            return;
599        }
600        //
601        mFirstExtendedDirectoryId = Long.MAX_VALUE;
602        if (numExtendedDirectories > 0) {
603            // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
604            // "special" ID.
605            long maxId = Directory.LOCAL_INVISIBLE;
606            int insertIndex = 0;
607            for (int i = 0, n = getPartitionCount(); i < n; i++) {
608                final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
609                final long id = partition.getDirectoryId();
610                if (id > maxId) {
611                    maxId = id;
612                }
613                if (!DirectoryCompat.isRemoteDirectoryId(id)) {
614                    // assuming remote directories come after local, we will end up with the index
615                    // where we should insert extended directories. This also works if there are no
616                    // remote directories at all.
617                    insertIndex = i + 1;
618                }
619            }
620            // Extended directories ID's cannot collide with base directories
621            mFirstExtendedDirectoryId = maxId + 1;
622            for (int i = 0; i < numExtendedDirectories; i++) {
623                final long id = mFirstExtendedDirectoryId + i;
624                final DirectoryPartition directory = mExtendedDirectories.get(i);
625                if (getPartitionByDirectoryId(id) == -1) {
626                    addPartition(insertIndex, directory);
627                    directory.setDirectoryId(id);
628                }
629            }
630        }
631    }
632
633    @Override
634    protected Uri getContactUri(int partitionIndex, Cursor cursor,
635            int contactIdColumn, int lookUpKeyColumn) {
636        final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex);
637        final long directoryId = directory.getDirectoryId();
638        if (!isExtendedDirectory(directoryId)) {
639            return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn);
640        }
641        return Contacts.CONTENT_LOOKUP_URI.buildUpon()
642                .appendPath(Constants.LOOKUP_URI_ENCODED)
643                .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel())
644                .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
645                        String.valueOf(directoryId))
646                .encodedFragment(cursor.getString(lookUpKeyColumn))
647                .build();
648    }
649
650    public Listener getListener() {
651        return mListener;
652    }
653
654    public void setListener(Listener listener) {
655        mListener = listener;
656    }
657}
658