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.ContentUris; 19 import android.content.Context; 20 import android.content.CursorLoader; 21 import android.content.SharedPreferences; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.net.Uri.Builder; 25 import android.preference.PreferenceManager; 26 import android.provider.ContactsContract; 27 import android.provider.ContactsContract.Contacts; 28 import android.provider.ContactsContract.Data; 29 import android.provider.ContactsContract.Directory; 30 import android.provider.ContactsContract.SearchSnippets; 31 import android.text.TextUtils; 32 import android.view.View; 33 34 import com.android.contacts.compat.ContactsCompat; 35 import com.android.contacts.model.account.AccountWithDataSet; 36 import com.android.contacts.preference.ContactsPreferences; 37 38 import java.util.ArrayList; 39 import java.util.List; 40 41 /** 42 * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. 43 */ 44 public class DefaultContactListAdapter extends ContactListAdapter { 45 46 public static final char SNIPPET_START_MATCH = '['; 47 public static final char SNIPPET_END_MATCH = ']'; 48 49 // Contacts contacted within the last 3 days (in seconds) 50 private static final long LAST_TIME_USED_3_DAYS_SEC = 3L * 24 * 60 * 60; 51 52 // Contacts contacted within the last 7 days (in seconds) 53 private static final long LAST_TIME_USED_7_DAYS_SEC = 7L * 24 * 60 * 60; 54 55 // Contacts contacted within the last 14 days (in seconds) 56 private static final long LAST_TIME_USED_14_DAYS_SEC = 14L * 24 * 60 * 60; 57 58 // Contacts contacted within the last 30 days (in seconds) 59 private static final long LAST_TIME_USED_30_DAYS_SEC = 30L * 24 * 60 * 60; 60 61 private static final String TIME_SINCE_LAST_USED_SEC = 62 "(strftime('%s', 'now') - " + Contacts.LAST_TIME_CONTACTED + "/1000)"; 63 64 private static final String STREQUENT_SORT = 65 "(CASE WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC + 66 " THEN 0 " + 67 " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC + 68 " THEN 1 " + 69 " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC + 70 " THEN 2 " + 71 " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC + 72 " THEN 3 " + 73 " ELSE 4 END), " + 74 Contacts.TIMES_CONTACTED + " DESC, " + 75 Contacts.STARRED + " DESC"; 76 77 public DefaultContactListAdapter(Context context) { 78 super(context); 79 } 80 81 @Override 82 public void configureLoader(CursorLoader loader, long directoryId) { 83 if (loader instanceof FavoritesAndContactsLoader) { 84 ((FavoritesAndContactsLoader) loader).setLoadFavorites(shouldIncludeFavorites()); 85 } 86 87 String sortOrder = null; 88 if (isSearchMode()) { 89 String query = getQueryString(); 90 if (query == null) query = ""; 91 query = query.trim(); 92 if (TextUtils.isEmpty(query)) { 93 // Regardless of the directory, we don't want anything returned, 94 // so let's just send a "nothing" query to the local directory. 95 loader.setUri(Contacts.CONTENT_URI); 96 loader.setProjection(getProjection(false)); 97 loader.setSelection("0"); 98 } else if (isGroupMembersFilter()) { 99 final ContactListFilter filter = getFilter(); 100 configureUri(loader, directoryId, filter); 101 // TODO: This is not the normal type to filter URI so we load the non-search 102 // projection. Consider creating a specific group member adapter to make it more 103 // clear. 104 loader.setProjection(getProjection(/* forSearch */ false)); 105 loader.setSelection( 106 Contacts.DISPLAY_NAME_PRIMARY + " LIKE ?1 OR " + 107 Contacts.DISPLAY_NAME_ALTERNATIVE + " LIKE ?1"); 108 final String[] args = new String[1]; 109 args[0] = query + "%"; 110 loader.setSelectionArgs(args); 111 } else { 112 final Builder builder = ContactsCompat.getContentUri().buildUpon(); 113 appendSearchParameters(builder, query, directoryId); 114 loader.setUri(builder.build()); 115 loader.setProjection(getProjection(true)); 116 sortOrder = STREQUENT_SORT; 117 } 118 } else { 119 final ContactListFilter filter = getFilter(); 120 configureUri(loader, directoryId, filter); 121 if (filter != null 122 && filter.filterType == ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS) { 123 loader.setProjection(getDataProjectionForContacts(false)); 124 } else { 125 loader.setProjection(getProjection(false)); 126 } 127 configureSelection(loader, directoryId, filter); 128 } 129 130 if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { 131 if (sortOrder == null) { 132 sortOrder = Contacts.SORT_KEY_PRIMARY; 133 } else { 134 sortOrder += ", " + Contacts.SORT_KEY_PRIMARY; 135 } 136 } else { 137 if (sortOrder == null) { 138 sortOrder = Contacts.SORT_KEY_ALTERNATIVE; 139 } else { 140 sortOrder += ", " + Contacts.SORT_KEY_ALTERNATIVE; 141 } 142 } 143 loader.setSortOrder(sortOrder); 144 } 145 146 private boolean isGroupMembersFilter() { 147 final ContactListFilter filter = getFilter(); 148 return filter != null && filter.filterType == ContactListFilter.FILTER_TYPE_GROUP_MEMBERS; 149 } 150 151 private void appendSearchParameters(Builder builder, String query, long directoryId) { 152 builder.appendPath(query); // Builder will encode the query 153 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 154 String.valueOf(directoryId)); 155 if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) { 156 builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 157 String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); 158 } 159 builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1"); 160 } 161 162 protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) { 163 Uri uri = Contacts.CONTENT_URI; 164 if (filter != null) { 165 if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { 166 String lookupKey = getSelectedContactLookupKey(); 167 if (lookupKey != null) { 168 uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey); 169 } else { 170 uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, getSelectedContactId()); 171 } 172 } else if (filter.filterType == ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS) { 173 uri = Data.CONTENT_URI; 174 } 175 } 176 177 if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) { 178 uri = ContactListAdapter.buildSectionIndexerUri(uri); 179 } 180 181 if (filter != null 182 && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM 183 && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { 184 final Uri.Builder builder = uri.buildUpon(); 185 if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT 186 || filter.filterType == ContactListFilter.FILTER_TYPE_GROUP_MEMBERS) { 187 filter.addAccountQueryParameterToUrl(builder); 188 } 189 uri = builder.build(); 190 } 191 192 loader.setUri(uri); 193 } 194 195 private void configureSelection( 196 CursorLoader loader, long directoryId, ContactListFilter filter) { 197 if (filter == null) { 198 return; 199 } 200 201 if (directoryId != Directory.DEFAULT) { 202 return; 203 } 204 205 StringBuilder selection = new StringBuilder(); 206 List<String> selectionArgs = new ArrayList<String>(); 207 208 switch (filter.filterType) { 209 case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: { 210 // We have already added directory=0 to the URI, which takes care of this 211 // filter 212 break; 213 } 214 case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: { 215 // We have already added the lookup key to the URI, which takes care of this 216 // filter 217 break; 218 } 219 case ContactListFilter.FILTER_TYPE_STARRED: { 220 selection.append(Contacts.STARRED + "!=0"); 221 break; 222 } 223 case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: { 224 selection.append(Contacts.HAS_PHONE_NUMBER + "=1"); 225 break; 226 } 227 case ContactListFilter.FILTER_TYPE_CUSTOM: { 228 selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); 229 if (isCustomFilterForPhoneNumbersOnly()) { 230 selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); 231 } 232 break; 233 } 234 case ContactListFilter.FILTER_TYPE_ACCOUNT: { 235 // We use query parameters for account filter, so no selection to add here. 236 break; 237 } 238 case ContactListFilter.FILTER_TYPE_GROUP_MEMBERS: { 239 break; 240 } 241 case ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS: { 242 if (filter.accountType != null) { 243 selection.append(ContactsContract.RawContacts.ACCOUNT_TYPE) 244 .append("=?"); 245 selectionArgs.add(filter.accountType); 246 if (filter.accountName != null) { 247 selection.append(" AND ").append(ContactsContract.RawContacts.ACCOUNT_NAME) 248 .append(("=?")); 249 selectionArgs.add(filter.accountName); 250 } 251 } else { 252 selection.append(AccountWithDataSet.LOCAL_ACCOUNT_SELECTION); 253 } 254 break; 255 } 256 } 257 loader.setSelection(selection.toString()); 258 loader.setSelectionArgs(selectionArgs.toArray(new String[0])); 259 } 260 261 @Override 262 protected void bindView(View itemView, int partition, Cursor cursor, int position) { 263 super.bindView(itemView, partition, cursor, position); 264 final ContactListItemView view = (ContactListItemView)itemView; 265 266 view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); 267 268 if (isSelectionVisible()) { 269 view.setActivated(isSelectedContact(partition, cursor)); 270 } 271 272 bindSectionHeaderAndDivider(view, position, cursor); 273 274 if (isQuickContactEnabled()) { 275 bindQuickContact(view, partition, cursor, ContactQuery.CONTACT_PHOTO_ID, 276 ContactQuery.CONTACT_PHOTO_URI, ContactQuery.CONTACT_ID, 277 ContactQuery.CONTACT_LOOKUP_KEY, ContactQuery.CONTACT_DISPLAY_NAME); 278 } else { 279 if (getDisplayPhotos()) { 280 bindPhoto(view, partition, cursor); 281 } 282 } 283 284 bindNameAndViewId(view, cursor); 285 bindPresenceAndStatusMessage(view, cursor); 286 287 if (isSearchMode()) { 288 bindSearchSnippet(view, cursor); 289 } else { 290 view.setSnippet(null); 291 } 292 } 293 294 private boolean isCustomFilterForPhoneNumbersOnly() { 295 // TODO: this flag should not be stored in shared prefs. It needs to be in the db. 296 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 297 return prefs.getBoolean(ContactsPreferences.PREF_DISPLAY_ONLY_PHONES, 298 ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT); 299 } 300 } 301