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