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