AggregationSuggestionEngine.java revision 6a87ee9b9ed26eb1cbc888be758ddf4f4e0694b8
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 17package com.android.contacts.editor; 18 19import android.content.ContentResolver; 20import android.content.Context; 21import android.database.ContentObserver; 22import android.database.Cursor; 23import android.net.Uri; 24import android.os.Build; 25import android.os.Handler; 26import android.os.HandlerThread; 27import android.os.Message; 28import android.os.Process; 29import android.provider.ContactsContract.CommonDataKinds.Email; 30import android.provider.ContactsContract.CommonDataKinds.Nickname; 31import android.provider.ContactsContract.CommonDataKinds.Phone; 32import android.provider.ContactsContract.CommonDataKinds.Photo; 33import android.provider.ContactsContract.CommonDataKinds.StructuredName; 34import android.provider.ContactsContract.Contacts; 35import android.provider.ContactsContract.Contacts.AggregationSuggestions; 36import android.provider.ContactsContract.Contacts.AggregationSuggestions.Builder; 37import android.provider.ContactsContract.Data; 38import android.provider.ContactsContract.RawContacts; 39import android.text.TextUtils; 40 41import com.android.contacts.common.model.ValuesDelta; 42import com.android.contacts.common.model.account.AccountWithDataSet; 43import com.android.contacts.compat.AggregationSuggestionsCompat; 44 45import com.google.common.base.MoreObjects; 46import com.google.common.collect.Lists; 47 48import java.util.ArrayList; 49import java.util.Arrays; 50import java.util.List; 51 52/** 53 * Runs asynchronous queries to obtain aggregation suggestions in the as-you-type mode. 54 */ 55public class AggregationSuggestionEngine extends HandlerThread { 56 public static final String TAG = "AggregationSuggestionEngine"; 57 58 public interface Listener { 59 void onAggregationSuggestionChange(); 60 } 61 62 public static final class Suggestion { 63 public long contactId; 64 public String contactLookupKey; 65 public long rawContactId; 66 public long photoId = -1; 67 public String name; 68 public String phoneNumber; 69 public String emailAddress; 70 public String nickname; 71 72 @Override 73 public String toString() { 74 return MoreObjects.toStringHelper(Suggestion.class) 75 .add("contactId", contactId) 76 .add("contactLookupKey", contactLookupKey) 77 .add("rawContactId", rawContactId) 78 .add("photoId", photoId) 79 .add("name", name) 80 .add("phoneNumber", phoneNumber) 81 .add("emailAddress", emailAddress) 82 .add("nickname", nickname) 83 .toString(); 84 } 85 } 86 87 private final class SuggestionContentObserver extends ContentObserver { 88 private SuggestionContentObserver(Handler handler) { 89 super(handler); 90 } 91 92 @Override 93 public void onChange(boolean selfChange) { 94 scheduleSuggestionLookup(); 95 } 96 } 97 98 private static final int MESSAGE_RESET = 0; 99 private static final int MESSAGE_NAME_CHANGE = 1; 100 private static final int MESSAGE_DATA_CURSOR = 2; 101 102 private static final long SUGGESTION_LOOKUP_DELAY_MILLIS = 300; 103 104 private static final int SUGGESTIONS_LIMIT = 3; 105 106 private final Context mContext; 107 108 private long[] mSuggestedContactIds = new long[0]; 109 private Handler mMainHandler; 110 private Handler mHandler; 111 private long mContactId; 112 private AccountWithDataSet mAccountFilter; 113 private Listener mListener; 114 private Cursor mDataCursor; 115 private ContentObserver mContentObserver; 116 private Uri mSuggestionsUri; 117 118 public AggregationSuggestionEngine(Context context) { 119 super("AggregationSuggestions", Process.THREAD_PRIORITY_BACKGROUND); 120 mContext = context.getApplicationContext(); 121 mMainHandler = new Handler() { 122 @Override 123 public void handleMessage(Message msg) { 124 AggregationSuggestionEngine.this.deliverNotification((Cursor) msg.obj); 125 } 126 }; 127 } 128 129 protected Handler getHandler() { 130 if (mHandler == null) { 131 mHandler = new Handler(getLooper()) { 132 @Override 133 public void handleMessage(Message msg) { 134 AggregationSuggestionEngine.this.handleMessage(msg); 135 } 136 }; 137 } 138 return mHandler; 139 } 140 141 public void setContactId(long contactId) { 142 if (contactId != mContactId) { 143 mContactId = contactId; 144 reset(); 145 } 146 } 147 148 public void setAccountFilter(AccountWithDataSet account) { 149 mAccountFilter = account; 150 } 151 152 public void setListener(Listener listener) { 153 mListener = listener; 154 } 155 156 @Override 157 public boolean quit() { 158 if (mDataCursor != null) { 159 mDataCursor.close(); 160 } 161 mDataCursor = null; 162 if (mContentObserver != null) { 163 mContext.getContentResolver().unregisterContentObserver(mContentObserver); 164 mContentObserver = null; 165 } 166 return super.quit(); 167 } 168 169 public void reset() { 170 Handler handler = getHandler(); 171 handler.removeMessages(MESSAGE_NAME_CHANGE); 172 handler.sendEmptyMessage(MESSAGE_RESET); 173 } 174 175 public void onNameChange(ValuesDelta values) { 176 mSuggestionsUri = buildAggregationSuggestionUri(values); 177 if (mSuggestionsUri != null) { 178 if (mContentObserver == null) { 179 mContentObserver = new SuggestionContentObserver(getHandler()); 180 mContext.getContentResolver().registerContentObserver( 181 Contacts.CONTENT_URI, true, mContentObserver); 182 } 183 } else if (mContentObserver != null) { 184 mContext.getContentResolver().unregisterContentObserver(mContentObserver); 185 mContentObserver = null; 186 } 187 scheduleSuggestionLookup(); 188 } 189 190 protected void scheduleSuggestionLookup() { 191 Handler handler = getHandler(); 192 handler.removeMessages(MESSAGE_NAME_CHANGE); 193 194 if (mSuggestionsUri == null) { 195 return; 196 } 197 198 Message msg = handler.obtainMessage(MESSAGE_NAME_CHANGE, mSuggestionsUri); 199 handler.sendMessageDelayed(msg, SUGGESTION_LOOKUP_DELAY_MILLIS); 200 } 201 202 private Uri buildAggregationSuggestionUri(ValuesDelta values) { 203 StringBuilder nameSb = new StringBuilder(); 204 appendValue(nameSb, values, StructuredName.PREFIX); 205 appendValue(nameSb, values, StructuredName.GIVEN_NAME); 206 appendValue(nameSb, values, StructuredName.MIDDLE_NAME); 207 appendValue(nameSb, values, StructuredName.FAMILY_NAME); 208 appendValue(nameSb, values, StructuredName.SUFFIX); 209 210 if (nameSb.length() == 0) { 211 appendValue(nameSb, values, StructuredName.DISPLAY_NAME); 212 } 213 214 StringBuilder phoneticNameSb = new StringBuilder(); 215 appendValue(phoneticNameSb, values, StructuredName.PHONETIC_FAMILY_NAME); 216 appendValue(phoneticNameSb, values, StructuredName.PHONETIC_MIDDLE_NAME); 217 appendValue(phoneticNameSb, values, StructuredName.PHONETIC_GIVEN_NAME); 218 219 if (nameSb.length() == 0 && phoneticNameSb.length() == 0) { 220 return null; 221 } 222 223 // AggregationSuggestions.Builder() became visible in API level 23, so use it if applicable. 224 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 225 final Builder uriBuilder = new AggregationSuggestions.Builder() 226 .setLimit(SUGGESTIONS_LIMIT) 227 .setContactId(mContactId); 228 if (nameSb.length() != 0) { 229 uriBuilder.addNameParameter(nameSb.toString()); 230 } 231 if (phoneticNameSb.length() != 0) { 232 uriBuilder.addNameParameter(phoneticNameSb.toString()); 233 } 234 return uriBuilder.build(); 235 } 236 237 // For previous SDKs, use the backup plan. 238 final AggregationSuggestionsCompat.Builder uriBuilder = 239 new AggregationSuggestionsCompat.Builder() 240 .setLimit(SUGGESTIONS_LIMIT) 241 .setContactId(mContactId); 242 if (nameSb.length() != 0) { 243 uriBuilder.addNameParameter(nameSb.toString()); 244 } 245 if (phoneticNameSb.length() != 0) { 246 uriBuilder.addNameParameter(phoneticNameSb.toString()); 247 } 248 return uriBuilder.build(); 249 } 250 251 private void appendValue(StringBuilder sb, ValuesDelta values, String column) { 252 String value = values.getAsString(column); 253 if (!TextUtils.isEmpty(value)) { 254 if (sb.length() > 0) { 255 sb.append(' '); 256 } 257 sb.append(value); 258 } 259 } 260 261 protected void handleMessage(Message msg) { 262 switch (msg.what) { 263 case MESSAGE_RESET: 264 mSuggestedContactIds = new long[0]; 265 break; 266 case MESSAGE_NAME_CHANGE: 267 loadAggregationSuggestions((Uri) msg.obj); 268 break; 269 } 270 } 271 272 private static final class DataQuery { 273 274 public static final String SELECTION_PREFIX = 275 Data.MIMETYPE + " IN ('" 276 + Phone.CONTENT_ITEM_TYPE + "','" 277 + Email.CONTENT_ITEM_TYPE + "','" 278 + StructuredName.CONTENT_ITEM_TYPE + "','" 279 + Nickname.CONTENT_ITEM_TYPE + "','" 280 + Photo.CONTENT_ITEM_TYPE + "')" 281 + " AND " + Data.CONTACT_ID + " IN ("; 282 283 public static final String[] COLUMNS = { 284 Data.CONTACT_ID, 285 Data.LOOKUP_KEY, 286 Data.RAW_CONTACT_ID, 287 Data.MIMETYPE, 288 Data.DATA1, 289 Data.IS_SUPER_PRIMARY, 290 RawContacts.ACCOUNT_TYPE, 291 RawContacts.ACCOUNT_NAME, 292 RawContacts.DATA_SET, 293 Contacts.Photo._ID 294 }; 295 296 public static final int CONTACT_ID = 0; 297 public static final int LOOKUP_KEY = 1; 298 public static final int RAW_CONTACT_ID = 2; 299 public static final int MIMETYPE = 3; 300 public static final int DATA1 = 4; 301 public static final int IS_SUPERPRIMARY = 5; 302 public static final int ACCOUNT_TYPE = 6; 303 public static final int ACCOUNT_NAME = 7; 304 public static final int DATA_SET = 8; 305 public static final int PHOTO_ID = 9; 306 } 307 308 private void loadAggregationSuggestions(Uri uri) { 309 ContentResolver contentResolver = mContext.getContentResolver(); 310 Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null); 311 if (cursor == null) { 312 return; 313 } 314 try { 315 // If a new request is pending, chuck the result of the previous request 316 if (getHandler().hasMessages(MESSAGE_NAME_CHANGE)) { 317 return; 318 } 319 320 boolean changed = updateSuggestedContactIds(cursor); 321 if (!changed) { 322 return; 323 } 324 325 StringBuilder sb = new StringBuilder(DataQuery.SELECTION_PREFIX); 326 int count = mSuggestedContactIds.length; 327 for (int i = 0; i < count; i++) { 328 if (i > 0) { 329 sb.append(','); 330 } 331 sb.append(mSuggestedContactIds[i]); 332 } 333 sb.append(')'); 334 335 Cursor dataCursor = contentResolver.query(Data.CONTENT_URI, 336 DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID); 337 if (dataCursor != null) { 338 mMainHandler.sendMessage( 339 mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor)); 340 } 341 } finally { 342 cursor.close(); 343 } 344 } 345 346 private boolean updateSuggestedContactIds(final Cursor cursor) { 347 final int count = cursor.getCount(); 348 boolean changed = count != mSuggestedContactIds.length; 349 final ArrayList<Long> newIds = new ArrayList<Long>(count); 350 while (cursor.moveToNext()) { 351 final long contactId = cursor.getLong(0); 352 if (!changed && Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) { 353 changed = true; 354 } 355 newIds.add(contactId); 356 } 357 358 if (changed) { 359 mSuggestedContactIds = new long[newIds.size()]; 360 int i = 0; 361 for (final Long newId : newIds) { 362 mSuggestedContactIds[i++] = newId; 363 } 364 Arrays.sort(mSuggestedContactIds); 365 } 366 367 return changed; 368 } 369 370 protected void deliverNotification(Cursor dataCursor) { 371 if (mDataCursor != null) { 372 mDataCursor.close(); 373 } 374 mDataCursor = dataCursor; 375 if (mListener != null) { 376 mListener.onAggregationSuggestionChange(); 377 } 378 } 379 380 public int getSuggestedContactCount() { 381 return mDataCursor != null ? mDataCursor.getCount() : 0; 382 } 383 384 public List<Suggestion> getSuggestions() { 385 final ArrayList<Suggestion> list = Lists.newArrayList(); 386 387 if (mDataCursor != null && mAccountFilter != null) { 388 Suggestion suggestion = null; 389 long currentRawContactId = -1; 390 mDataCursor.moveToPosition(-1); 391 while (mDataCursor.moveToNext()) { 392 final long rawContactId = mDataCursor.getLong(DataQuery.RAW_CONTACT_ID); 393 if (rawContactId != currentRawContactId) { 394 suggestion = new Suggestion(); 395 suggestion.rawContactId = rawContactId; 396 suggestion.contactId = mDataCursor.getLong(DataQuery.CONTACT_ID); 397 suggestion.contactLookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY); 398 final String accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME); 399 final String accountType = mDataCursor.getString(DataQuery.ACCOUNT_TYPE); 400 final String dataSet = mDataCursor.getString(DataQuery.DATA_SET); 401 final AccountWithDataSet account = new AccountWithDataSet( 402 accountName, accountType, dataSet); 403 if (mAccountFilter.equals(account)) { 404 list.add(suggestion); 405 } 406 currentRawContactId = rawContactId; 407 } 408 409 final String mimetype = mDataCursor.getString(DataQuery.MIMETYPE); 410 if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) { 411 final String data = mDataCursor.getString(DataQuery.DATA1); 412 int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY); 413 if (!TextUtils.isEmpty(data) 414 && (superprimary != 0 || suggestion.phoneNumber == null)) { 415 suggestion.phoneNumber = data; 416 } 417 } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) { 418 final String data = mDataCursor.getString(DataQuery.DATA1); 419 int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY); 420 if (!TextUtils.isEmpty(data) 421 && (superprimary != 0 || suggestion.emailAddress == null)) { 422 suggestion.emailAddress = data; 423 } 424 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) { 425 final String data = mDataCursor.getString(DataQuery.DATA1); 426 if (!TextUtils.isEmpty(data)) { 427 suggestion.nickname = data; 428 } 429 } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) { 430 // DATA1 stores the display name for the raw contact. 431 final String data = mDataCursor.getString(DataQuery.DATA1); 432 if (!TextUtils.isEmpty(data) && suggestion.name == null) { 433 suggestion.name = data; 434 } 435 } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) { 436 final Long id = mDataCursor.getLong(DataQuery.PHOTO_ID); 437 if (suggestion.photoId == -1) { 438 suggestion.photoId = id; 439 } 440 } 441 } 442 } 443 return list; 444 } 445} 446