| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.contacts.editor; |
| |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Message; |
| import android.os.Process; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Nickname; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.Photo; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredName; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Contacts.AggregationSuggestions; |
| import android.provider.ContactsContract.Contacts.AggregationSuggestions.Builder; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.RawContacts; |
| import android.text.TextUtils; |
| |
| import com.android.contacts.compat.AggregationSuggestionsCompat; |
| import com.android.contacts.model.ValuesDelta; |
| import com.android.contacts.model.account.AccountWithDataSet; |
| |
| import com.google.common.base.MoreObjects; |
| import com.google.common.collect.Lists; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Runs asynchronous queries to obtain aggregation suggestions in the as-you-type mode. |
| */ |
| public class AggregationSuggestionEngine extends HandlerThread { |
| public interface Listener { |
| void onAggregationSuggestionChange(); |
| } |
| |
| public static final class Suggestion { |
| public long contactId; |
| public String contactLookupKey; |
| public long rawContactId; |
| public long photoId = -1; |
| public String name; |
| public String phoneNumber; |
| public String emailAddress; |
| public String nickname; |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(Suggestion.class) |
| .add("contactId", contactId) |
| .add("contactLookupKey", contactLookupKey) |
| .add("rawContactId", rawContactId) |
| .add("photoId", photoId) |
| .add("name", name) |
| .add("phoneNumber", phoneNumber) |
| .add("emailAddress", emailAddress) |
| .add("nickname", nickname) |
| .toString(); |
| } |
| } |
| |
| private final class SuggestionContentObserver extends ContentObserver { |
| private SuggestionContentObserver(Handler handler) { |
| super(handler); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| scheduleSuggestionLookup(); |
| } |
| } |
| |
| private static final int MESSAGE_RESET = 0; |
| private static final int MESSAGE_NAME_CHANGE = 1; |
| private static final int MESSAGE_DATA_CURSOR = 2; |
| |
| private static final long SUGGESTION_LOOKUP_DELAY_MILLIS = 300; |
| |
| private static final int SUGGESTIONS_LIMIT = 3; |
| |
| private final Context mContext; |
| |
| private long[] mSuggestedContactIds = new long[0]; |
| private Handler mMainHandler; |
| private Handler mHandler; |
| private long mContactId; |
| private AccountWithDataSet mAccountFilter; |
| private Listener mListener; |
| private Cursor mDataCursor; |
| private ContentObserver mContentObserver; |
| private Uri mSuggestionsUri; |
| |
| public AggregationSuggestionEngine(Context context) { |
| super("AggregationSuggestions", Process.THREAD_PRIORITY_BACKGROUND); |
| mContext = context.getApplicationContext(); |
| mMainHandler = new Handler() { |
| @Override |
| public void handleMessage(Message msg) { |
| AggregationSuggestionEngine.this.deliverNotification((Cursor) msg.obj); |
| } |
| }; |
| } |
| |
| protected Handler getHandler() { |
| if (mHandler == null) { |
| mHandler = new Handler(getLooper()) { |
| @Override |
| public void handleMessage(Message msg) { |
| AggregationSuggestionEngine.this.handleMessage(msg); |
| } |
| }; |
| } |
| return mHandler; |
| } |
| |
| public void setContactId(long contactId) { |
| if (contactId != mContactId) { |
| mContactId = contactId; |
| reset(); |
| } |
| } |
| |
| public void setAccountFilter(AccountWithDataSet account) { |
| mAccountFilter = account; |
| } |
| |
| public void setListener(Listener listener) { |
| mListener = listener; |
| } |
| |
| @Override |
| public boolean quit() { |
| if (mDataCursor != null) { |
| mDataCursor.close(); |
| } |
| mDataCursor = null; |
| if (mContentObserver != null) { |
| mContext.getContentResolver().unregisterContentObserver(mContentObserver); |
| mContentObserver = null; |
| } |
| return super.quit(); |
| } |
| |
| public void reset() { |
| Handler handler = getHandler(); |
| handler.removeMessages(MESSAGE_NAME_CHANGE); |
| handler.sendEmptyMessage(MESSAGE_RESET); |
| } |
| |
| public void onNameChange(ValuesDelta values) { |
| mSuggestionsUri = buildAggregationSuggestionUri(values); |
| if (mSuggestionsUri != null) { |
| if (mContentObserver == null) { |
| mContentObserver = new SuggestionContentObserver(getHandler()); |
| mContext.getContentResolver().registerContentObserver( |
| Contacts.CONTENT_URI, true, mContentObserver); |
| } |
| } else if (mContentObserver != null) { |
| mContext.getContentResolver().unregisterContentObserver(mContentObserver); |
| mContentObserver = null; |
| } |
| scheduleSuggestionLookup(); |
| } |
| |
| protected void scheduleSuggestionLookup() { |
| Handler handler = getHandler(); |
| handler.removeMessages(MESSAGE_NAME_CHANGE); |
| |
| if (mSuggestionsUri == null) { |
| return; |
| } |
| |
| Message msg = handler.obtainMessage(MESSAGE_NAME_CHANGE, mSuggestionsUri); |
| handler.sendMessageDelayed(msg, SUGGESTION_LOOKUP_DELAY_MILLIS); |
| } |
| |
| private Uri buildAggregationSuggestionUri(ValuesDelta values) { |
| StringBuilder nameSb = new StringBuilder(); |
| appendValue(nameSb, values, StructuredName.PREFIX); |
| appendValue(nameSb, values, StructuredName.GIVEN_NAME); |
| appendValue(nameSb, values, StructuredName.MIDDLE_NAME); |
| appendValue(nameSb, values, StructuredName.FAMILY_NAME); |
| appendValue(nameSb, values, StructuredName.SUFFIX); |
| |
| StringBuilder phoneticNameSb = new StringBuilder(); |
| appendValue(phoneticNameSb, values, StructuredName.PHONETIC_FAMILY_NAME); |
| appendValue(phoneticNameSb, values, StructuredName.PHONETIC_MIDDLE_NAME); |
| appendValue(phoneticNameSb, values, StructuredName.PHONETIC_GIVEN_NAME); |
| |
| if (nameSb.length() == 0 && phoneticNameSb.length() == 0) { |
| return null; |
| } |
| |
| // AggregationSuggestions.Builder() became visible in API level 23, so use it if applicable. |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| final Builder uriBuilder = new AggregationSuggestions.Builder() |
| .setLimit(SUGGESTIONS_LIMIT) |
| .setContactId(mContactId); |
| if (nameSb.length() != 0) { |
| uriBuilder.addNameParameter(nameSb.toString()); |
| } |
| if (phoneticNameSb.length() != 0) { |
| uriBuilder.addNameParameter(phoneticNameSb.toString()); |
| } |
| return uriBuilder.build(); |
| } |
| |
| // For previous SDKs, use the backup plan. |
| final AggregationSuggestionsCompat.Builder uriBuilder = |
| new AggregationSuggestionsCompat.Builder() |
| .setLimit(SUGGESTIONS_LIMIT) |
| .setContactId(mContactId); |
| if (nameSb.length() != 0) { |
| uriBuilder.addNameParameter(nameSb.toString()); |
| } |
| if (phoneticNameSb.length() != 0) { |
| uriBuilder.addNameParameter(phoneticNameSb.toString()); |
| } |
| return uriBuilder.build(); |
| } |
| |
| private void appendValue(StringBuilder sb, ValuesDelta values, String column) { |
| String value = values.getAsString(column); |
| if (!TextUtils.isEmpty(value)) { |
| if (sb.length() > 0) { |
| sb.append(' '); |
| } |
| sb.append(value); |
| } |
| } |
| |
| protected void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MESSAGE_RESET: |
| mSuggestedContactIds = new long[0]; |
| break; |
| case MESSAGE_NAME_CHANGE: |
| loadAggregationSuggestions((Uri) msg.obj); |
| break; |
| } |
| } |
| |
| private static final class DataQuery { |
| |
| public static final String SELECTION_PREFIX = |
| Data.MIMETYPE + " IN ('" |
| + Phone.CONTENT_ITEM_TYPE + "','" |
| + Email.CONTENT_ITEM_TYPE + "','" |
| + StructuredName.CONTENT_ITEM_TYPE + "','" |
| + Nickname.CONTENT_ITEM_TYPE + "','" |
| + Photo.CONTENT_ITEM_TYPE + "')" |
| + " AND " + Data.CONTACT_ID + " IN ("; |
| |
| public static final String[] COLUMNS = { |
| Data.CONTACT_ID, |
| Data.LOOKUP_KEY, |
| Data.RAW_CONTACT_ID, |
| Data.MIMETYPE, |
| Data.DATA1, |
| Data.IS_SUPER_PRIMARY, |
| RawContacts.ACCOUNT_TYPE, |
| RawContacts.ACCOUNT_NAME, |
| RawContacts.DATA_SET, |
| Contacts.Photo._ID |
| }; |
| |
| public static final int CONTACT_ID = 0; |
| public static final int LOOKUP_KEY = 1; |
| public static final int RAW_CONTACT_ID = 2; |
| public static final int MIMETYPE = 3; |
| public static final int DATA1 = 4; |
| public static final int IS_SUPERPRIMARY = 5; |
| public static final int ACCOUNT_TYPE = 6; |
| public static final int ACCOUNT_NAME = 7; |
| public static final int DATA_SET = 8; |
| public static final int PHOTO_ID = 9; |
| } |
| |
| private void loadAggregationSuggestions(Uri uri) { |
| ContentResolver contentResolver = mContext.getContentResolver(); |
| Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null); |
| if (cursor == null) { |
| return; |
| } |
| try { |
| // If a new request is pending, chuck the result of the previous request |
| if (getHandler().hasMessages(MESSAGE_NAME_CHANGE)) { |
| return; |
| } |
| |
| boolean changed = updateSuggestedContactIds(cursor); |
| if (!changed) { |
| return; |
| } |
| |
| StringBuilder sb = new StringBuilder(DataQuery.SELECTION_PREFIX); |
| int count = mSuggestedContactIds.length; |
| for (int i = 0; i < count; i++) { |
| if (i > 0) { |
| sb.append(','); |
| } |
| sb.append(mSuggestedContactIds[i]); |
| } |
| sb.append(')'); |
| |
| Cursor dataCursor = contentResolver.query(Data.CONTENT_URI, |
| DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID); |
| if (dataCursor != null) { |
| mMainHandler.sendMessage( |
| mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor)); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| private boolean updateSuggestedContactIds(final Cursor cursor) { |
| final int count = cursor.getCount(); |
| boolean changed = count != mSuggestedContactIds.length; |
| final ArrayList<Long> newIds = new ArrayList<Long>(count); |
| while (cursor.moveToNext()) { |
| final long contactId = cursor.getLong(0); |
| if (!changed && Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) { |
| changed = true; |
| } |
| newIds.add(contactId); |
| } |
| |
| if (changed) { |
| mSuggestedContactIds = new long[newIds.size()]; |
| int i = 0; |
| for (final Long newId : newIds) { |
| mSuggestedContactIds[i++] = newId; |
| } |
| Arrays.sort(mSuggestedContactIds); |
| } |
| |
| return changed; |
| } |
| |
| protected void deliverNotification(Cursor dataCursor) { |
| if (mDataCursor != null) { |
| mDataCursor.close(); |
| } |
| mDataCursor = dataCursor; |
| if (mListener != null) { |
| mListener.onAggregationSuggestionChange(); |
| } |
| } |
| |
| public int getSuggestedContactCount() { |
| return mDataCursor != null ? mDataCursor.getCount() : 0; |
| } |
| |
| public List<Suggestion> getSuggestions() { |
| final ArrayList<Suggestion> list = Lists.newArrayList(); |
| |
| if (mDataCursor != null && mAccountFilter != null) { |
| Suggestion suggestion = null; |
| long currentRawContactId = -1; |
| mDataCursor.moveToPosition(-1); |
| while (mDataCursor.moveToNext()) { |
| final long rawContactId = mDataCursor.getLong(DataQuery.RAW_CONTACT_ID); |
| if (rawContactId != currentRawContactId) { |
| suggestion = new Suggestion(); |
| suggestion.rawContactId = rawContactId; |
| suggestion.contactId = mDataCursor.getLong(DataQuery.CONTACT_ID); |
| suggestion.contactLookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY); |
| final String accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME); |
| final String accountType = mDataCursor.getString(DataQuery.ACCOUNT_TYPE); |
| final String dataSet = mDataCursor.getString(DataQuery.DATA_SET); |
| final AccountWithDataSet account = new AccountWithDataSet( |
| accountName, accountType, dataSet); |
| if (mAccountFilter.equals(account)) { |
| list.add(suggestion); |
| } |
| currentRawContactId = rawContactId; |
| } |
| |
| final String mimetype = mDataCursor.getString(DataQuery.MIMETYPE); |
| if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) { |
| final String data = mDataCursor.getString(DataQuery.DATA1); |
| int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY); |
| if (!TextUtils.isEmpty(data) |
| && (superprimary != 0 || suggestion.phoneNumber == null)) { |
| suggestion.phoneNumber = data; |
| } |
| } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) { |
| final String data = mDataCursor.getString(DataQuery.DATA1); |
| int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY); |
| if (!TextUtils.isEmpty(data) |
| && (superprimary != 0 || suggestion.emailAddress == null)) { |
| suggestion.emailAddress = data; |
| } |
| } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) { |
| final String data = mDataCursor.getString(DataQuery.DATA1); |
| if (!TextUtils.isEmpty(data)) { |
| suggestion.nickname = data; |
| } |
| } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) { |
| // DATA1 stores the display name for the raw contact. |
| final String data = mDataCursor.getString(DataQuery.DATA1); |
| if (!TextUtils.isEmpty(data) && suggestion.name == null) { |
| suggestion.name = data; |
| } |
| } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) { |
| final Long id = mDataCursor.getLong(DataQuery.PHOTO_ID); |
| if (suggestion.photoId == -1) { |
| suggestion.photoId = id; |
| } |
| } |
| } |
| } |
| return list; |
| } |
| } |