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