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