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.Context;
19import android.database.Cursor;
20import android.net.Uri;
21import android.provider.ContactsContract;
22import android.provider.ContactsContract.Contacts;
23import android.provider.ContactsContract.Directory;
24import android.provider.ContactsContract.SearchSnippets;
25import android.text.TextUtils;
26import android.view.ViewGroup;
27import android.widget.ListView;
28
29import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
30import com.android.contacts.common.R;
31import com.android.contacts.common.compat.ContactsCompat;
32import com.android.contacts.common.preference.ContactsPreferences;
33
34/**
35 * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
36 * Also includes support for including the {@link ContactsContract.Profile} record in the
37 * list.
38 */
39public abstract class ContactListAdapter extends ContactEntryListAdapter {
40
41    protected static class ContactQuery {
42        private static final String[] CONTACT_PROJECTION_PRIMARY = new String[] {
43            Contacts._ID,                           // 0
44            Contacts.DISPLAY_NAME_PRIMARY,          // 1
45            Contacts.CONTACT_PRESENCE,              // 2
46            Contacts.CONTACT_STATUS,                // 3
47            Contacts.PHOTO_ID,                      // 4
48            Contacts.PHOTO_THUMBNAIL_URI,           // 5
49            Contacts.LOOKUP_KEY,                    // 6
50            Contacts.IS_USER_PROFILE,               // 7
51            Contacts.PHONETIC_NAME,                 // 8
52        };
53
54        private static final String[] CONTACT_PROJECTION_ALTERNATIVE = new String[] {
55            Contacts._ID,                           // 0
56            Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
57            Contacts.CONTACT_PRESENCE,              // 2
58            Contacts.CONTACT_STATUS,                // 3
59            Contacts.PHOTO_ID,                      // 4
60            Contacts.PHOTO_THUMBNAIL_URI,           // 5
61            Contacts.LOOKUP_KEY,                    // 6
62            Contacts.IS_USER_PROFILE,               // 7
63            Contacts.PHONETIC_NAME,                 // 8
64        };
65
66        private static final String[] FILTER_PROJECTION_PRIMARY = new String[] {
67            Contacts._ID,                           // 0
68            Contacts.DISPLAY_NAME_PRIMARY,          // 1
69            Contacts.CONTACT_PRESENCE,              // 2
70            Contacts.CONTACT_STATUS,                // 3
71            Contacts.PHOTO_ID,                      // 4
72            Contacts.PHOTO_THUMBNAIL_URI,           // 5
73            Contacts.LOOKUP_KEY,                    // 6
74            Contacts.IS_USER_PROFILE,               // 7
75            Contacts.PHONETIC_NAME,                 // 8
76            Contacts.LAST_TIME_CONTACTED,           // 9
77            Contacts.STARRED,                       // 10
78            SearchSnippets.SNIPPET,                 // 11
79        };
80
81        private static final String[] FILTER_PROJECTION_ALTERNATIVE = new String[] {
82            Contacts._ID,                           // 0
83            Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
84            Contacts.CONTACT_PRESENCE,              // 2
85            Contacts.CONTACT_STATUS,                // 3
86            Contacts.PHOTO_ID,                      // 4
87            Contacts.PHOTO_THUMBNAIL_URI,           // 5
88            Contacts.LOOKUP_KEY,                    // 6
89            Contacts.IS_USER_PROFILE,               // 7
90            Contacts.PHONETIC_NAME,                 // 8
91            Contacts.LAST_TIME_CONTACTED,           // 9
92            Contacts.STARRED,                       // 10
93            SearchSnippets.SNIPPET,                 // 11
94        };
95
96        public static final int CONTACT_ID               = 0;
97        public static final int CONTACT_DISPLAY_NAME     = 1;
98        public static final int CONTACT_PRESENCE_STATUS  = 2;
99        public static final int CONTACT_CONTACT_STATUS   = 3;
100        public static final int CONTACT_PHOTO_ID         = 4;
101        public static final int CONTACT_PHOTO_URI        = 5;
102        public static final int CONTACT_LOOKUP_KEY       = 6;
103        public static final int CONTACT_IS_USER_PROFILE  = 7;
104        public static final int CONTACT_PHONETIC_NAME    = 8;
105        public static final int CONTACT_LAST_TIME_CONTACTED = 9;
106        public static final int CONTACT_STARRED          = 10;
107        public static final int CONTACT_SNIPPET          = 11;
108    }
109
110    private CharSequence mUnknownNameText;
111
112    private long mSelectedContactDirectoryId;
113    private String mSelectedContactLookupKey;
114    private long mSelectedContactId;
115    private ContactListItemView.PhotoPosition mPhotoPosition;
116
117    public ContactListAdapter(Context context) {
118        super(context);
119
120        mUnknownNameText = context.getText(R.string.missing_name);
121    }
122
123    public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
124        mPhotoPosition = photoPosition;
125    }
126
127    public ContactListItemView.PhotoPosition getPhotoPosition() {
128        return mPhotoPosition;
129    }
130
131    public CharSequence getUnknownNameText() {
132        return mUnknownNameText;
133    }
134
135    public long getSelectedContactDirectoryId() {
136        return mSelectedContactDirectoryId;
137    }
138
139    public String getSelectedContactLookupKey() {
140        return mSelectedContactLookupKey;
141    }
142
143    public long getSelectedContactId() {
144        return mSelectedContactId;
145    }
146
147    public void setSelectedContact(long selectedDirectoryId, String lookupKey, long contactId) {
148        mSelectedContactDirectoryId = selectedDirectoryId;
149        mSelectedContactLookupKey = lookupKey;
150        mSelectedContactId = contactId;
151    }
152
153    protected static Uri buildSectionIndexerUri(Uri uri) {
154        return uri.buildUpon()
155                .appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build();
156    }
157
158    @Override
159    public String getContactDisplayName(int position) {
160        return ((Cursor) getItem(position)).getString(ContactQuery.CONTACT_DISPLAY_NAME);
161    }
162
163    /**
164     * Builds the {@link Contacts#CONTENT_LOOKUP_URI} for the given
165     * {@link ListView} position.
166     */
167    public Uri getContactUri(int position) {
168        int partitionIndex = getPartitionForPosition(position);
169        Cursor item = (Cursor)getItem(position);
170        return item != null ? getContactUri(partitionIndex, item) : null;
171    }
172
173    public Uri getContactUri(int partitionIndex, Cursor cursor) {
174        long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
175        String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
176        Uri uri = Contacts.getLookupUri(contactId, lookupKey);
177        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
178        if (uri != null && directoryId != Directory.DEFAULT) {
179            uri = uri.buildUpon().appendQueryParameter(
180                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
181        }
182        return uri;
183    }
184
185    public boolean isEnterpriseContact(int position) {
186        final Cursor cursor = (Cursor) getItem(position);
187        if (cursor != null) {
188            final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
189            return ContactsCompat.isEnterpriseContactId(contactId);
190        }
191        return false;
192    }
193
194    /**
195     * Returns true if the specified contact is selected in the list. For a
196     * contact to be shown as selected, we need both the directory and and the
197     * lookup key to be the same. We are paying no attention to the contactId,
198     * because it is volatile, especially in the case of directories.
199     */
200    public boolean isSelectedContact(int partitionIndex, Cursor cursor) {
201        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
202        if (getSelectedContactDirectoryId() != directoryId) {
203            return false;
204        }
205        String lookupKey = getSelectedContactLookupKey();
206        if (lookupKey != null && TextUtils.equals(lookupKey,
207                cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY))) {
208            return true;
209        }
210
211        return directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
212                && getSelectedContactId() == cursor.getLong(ContactQuery.CONTACT_ID);
213    }
214
215    @Override
216    protected ContactListItemView newView(
217            Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
218        ContactListItemView view = super.newView(context, partition, cursor, position, parent);
219        view.setUnknownNameText(mUnknownNameText);
220        view.setQuickContactEnabled(isQuickContactEnabled());
221        view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
222        view.setActivatedStateSupported(isSelectionVisible());
223        if (mPhotoPosition != null) {
224            view.setPhotoPosition(mPhotoPosition);
225        }
226        return view;
227    }
228
229    protected void bindSectionHeaderAndDivider(ContactListItemView view, int position,
230            Cursor cursor) {
231        view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
232        if (isSectionHeaderDisplayEnabled()) {
233            Placement placement = getItemPlacementInSection(position);
234            view.setSectionHeader(placement.sectionHeader);
235        } else {
236            view.setSectionHeader(null);
237        }
238    }
239
240    protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
241        if (!isPhotoSupported(partitionIndex)) {
242            view.removePhotoView();
243            return;
244        }
245
246        // Set the photo, if available
247        long photoId = 0;
248        if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) {
249            photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID);
250        }
251
252        if (photoId != 0) {
253            getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false,
254                    getCircularPhotos(), null);
255        } else {
256            final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI);
257            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
258            DefaultImageRequest request = null;
259            if (photoUri == null) {
260                request = getDefaultImageRequestFromCursor(cursor,
261                        ContactQuery.CONTACT_DISPLAY_NAME,
262                        ContactQuery.CONTACT_LOOKUP_KEY);
263            }
264            getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
265                    getCircularPhotos(), request);
266        }
267    }
268
269    protected void bindNameAndViewId(final ContactListItemView view, Cursor cursor) {
270        view.showDisplayName(
271                cursor, ContactQuery.CONTACT_DISPLAY_NAME, getContactNameDisplayOrder());
272        // Note: we don't show phonetic any more (See issue 5265330)
273
274        bindViewId(view, cursor, ContactQuery.CONTACT_ID);
275    }
276
277    protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) {
278        view.showPresenceAndStatusMessage(cursor, ContactQuery.CONTACT_PRESENCE_STATUS,
279                ContactQuery.CONTACT_CONTACT_STATUS);
280    }
281
282    protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
283        view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET);
284    }
285
286    public int getSelectedContactPosition() {
287        if (mSelectedContactLookupKey == null && mSelectedContactId == 0) {
288            return -1;
289        }
290
291        Cursor cursor = null;
292        int partitionIndex = -1;
293        int partitionCount = getPartitionCount();
294        for (int i = 0; i < partitionCount; i++) {
295            DirectoryPartition partition = (DirectoryPartition) getPartition(i);
296            if (partition.getDirectoryId() == mSelectedContactDirectoryId) {
297                partitionIndex = i;
298                break;
299            }
300        }
301        if (partitionIndex == -1) {
302            return -1;
303        }
304
305        cursor = getCursor(partitionIndex);
306        if (cursor == null) {
307            return -1;
308        }
309
310        cursor.moveToPosition(-1);      // Reset cursor
311        int offset = -1;
312        while (cursor.moveToNext()) {
313            if (mSelectedContactLookupKey != null) {
314                String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
315                if (mSelectedContactLookupKey.equals(lookupKey)) {
316                    offset = cursor.getPosition();
317                    break;
318                }
319            }
320            if (mSelectedContactId != 0 && (mSelectedContactDirectoryId == Directory.DEFAULT
321                    || mSelectedContactDirectoryId == Directory.LOCAL_INVISIBLE)) {
322                long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
323                if (contactId == mSelectedContactId) {
324                    offset = cursor.getPosition();
325                    break;
326                }
327            }
328        }
329        if (offset == -1) {
330            return -1;
331        }
332
333        int position = getPositionForPartition(partitionIndex) + offset;
334        if (hasHeader(partitionIndex)) {
335            position++;
336        }
337        return position;
338    }
339
340    public boolean hasValidSelection() {
341        return getSelectedContactPosition() != -1;
342    }
343
344    public Uri getFirstContactUri() {
345        int partitionCount = getPartitionCount();
346        for (int i = 0; i < partitionCount; i++) {
347            DirectoryPartition partition = (DirectoryPartition) getPartition(i);
348            if (partition.isLoading()) {
349                continue;
350            }
351
352            Cursor cursor = getCursor(i);
353            if (cursor == null) {
354                continue;
355            }
356
357            if (!cursor.moveToFirst()) {
358                continue;
359            }
360
361            return getContactUri(i, cursor);
362        }
363
364        return null;
365    }
366
367    @Override
368    public void changeCursor(int partitionIndex, Cursor cursor) {
369        super.changeCursor(partitionIndex, cursor);
370
371        // Check if a profile exists
372        if (cursor != null && cursor.moveToFirst()) {
373            setProfileExists(cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1);
374        }
375    }
376
377    /**
378     * @return Projection useful for children.
379     */
380    protected final String[] getProjection(boolean forSearch) {
381        final int sortOrder = getContactNameDisplayOrder();
382        if (forSearch) {
383            if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
384                return ContactQuery.FILTER_PROJECTION_PRIMARY;
385            } else {
386                return ContactQuery.FILTER_PROJECTION_ALTERNATIVE;
387            }
388        } else {
389            if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
390                return ContactQuery.CONTACT_PROJECTION_PRIMARY;
391            } else {
392                return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE;
393            }
394        }
395    }
396}
397