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