/* * 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 com.android.contacts.model.EntityDelta.ValuesDelta; import com.google.android.collect.Lists; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; 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 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 static final String TAG = "AggregationSuggestionEngine"; public interface Listener { void onAggregationSuggestionChange(); } public static final class RawContact { public long rawContactId; public String accountType; public String accountName; public String dataSet; @Override public String toString() { return "ID: " + rawContactId + " account: " + accountType + "/" + accountName + " dataSet: " + dataSet; } } public static final class Suggestion { public long contactId; public String lookupKey; public String name; public String phoneNumber; public String emailAddress; public String nickname; public byte[] photo; public List<RawContact> rawContacts; @Override public String toString() { return "ID: " + contactId + " rawContacts: " + rawContacts + " name: " + name + " phone: " + phoneNumber + " email: " + emailAddress + " nickname: " + nickname + (photo != null ? " [has photo]" : ""); } } 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 MAX_SUGGESTION_COUNT = 3; private final Context mContext; private long[] mSuggestedContactIds = new long[0]; private Handler mMainHandler; private Handler mHandler; private long mContactId; private Listener mListener; private Cursor mDataCursor; private ContentObserver mContentObserver; private Uri mSuggestionsUri; public AggregationSuggestionEngine(Context context) { super("AggregationSuggestions", Process.THREAD_PRIORITY_BACKGROUND); mContext = context; 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 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); if (nameSb.length() == 0) { appendValue(nameSb, values, StructuredName.DISPLAY_NAME); } 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; } Builder builder = AggregationSuggestions.builder() .setLimit(MAX_SUGGESTION_COUNT) .setContactId(mContactId); if (nameSb.length() != 0) { builder.addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, nameSb.toString()); } if (phoneticNameSb.length() != 0) { builder.addParameter( AggregationSuggestions.PARAMETER_MATCH_NAME, phoneticNameSb.toString()); } return builder.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._ID, Data.CONTACT_ID, Data.LOOKUP_KEY, Data.PHOTO_ID, Data.DISPLAY_NAME, Data.RAW_CONTACT_ID, Data.MIMETYPE, Data.DATA1, Data.IS_SUPER_PRIMARY, Photo.PHOTO, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME, RawContacts.DATA_SET }; public static final int ID = 0; public static final int CONTACT_ID = 1; public static final int LOOKUP_KEY = 2; public static final int PHOTO_ID = 3; public static final int DISPLAY_NAME = 4; public static final int RAW_CONTACT_ID = 5; public static final int MIMETYPE = 6; public static final int DATA1 = 7; public static final int IS_SUPERPRIMARY = 8; public static final int PHOTO = 9; public static final int ACCOUNT_TYPE = 10; public static final int ACCOUNT_NAME = 11; public static final int DATA_SET = 12; } private void loadAggregationSuggestions(Uri uri) { ContentResolver contentResolver = mContext.getContentResolver(); Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null); 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(')'); sb.toString(); Cursor dataCursor = contentResolver.query(Data.CONTENT_URI, DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID); mMainHandler.sendMessage(mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor)); } finally { cursor.close(); } } private boolean updateSuggestedContactIds(Cursor cursor) { int count = cursor.getCount(); boolean changed = count != mSuggestedContactIds.length; if (!changed) { while (cursor.moveToNext()) { long contactId = cursor.getLong(0); if (Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) { changed = true; break; } } } if (changed) { mSuggestedContactIds = new long[count]; cursor.moveToPosition(-1); for (int i = 0; i < count; i++) { cursor.moveToNext(); mSuggestedContactIds[i] = cursor.getLong(0); } 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() { ArrayList<Suggestion> list = Lists.newArrayList(); if (mDataCursor != null) { Suggestion suggestion = null; long currentContactId = -1; mDataCursor.moveToPosition(-1); while (mDataCursor.moveToNext()) { long contactId = mDataCursor.getLong(DataQuery.CONTACT_ID); if (contactId != currentContactId) { suggestion = new Suggestion(); suggestion.contactId = contactId; suggestion.name = mDataCursor.getString(DataQuery.DISPLAY_NAME); suggestion.lookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY); suggestion.rawContacts = Lists.newArrayList(); list.add(suggestion); currentContactId = contactId; } long rawContactId = mDataCursor.getLong(DataQuery.RAW_CONTACT_ID); if (!containsRawContact(suggestion, rawContactId)) { RawContact rawContact = new RawContact(); rawContact.rawContactId = rawContactId; rawContact.accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME); rawContact.accountType = mDataCursor.getString(DataQuery.ACCOUNT_TYPE); rawContact.dataSet = mDataCursor.getString(DataQuery.DATA_SET); suggestion.rawContacts.add(rawContact); } String mimetype = mDataCursor.getString(DataQuery.MIMETYPE); if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) { 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)) { 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)) { String data = mDataCursor.getString(DataQuery.DATA1); if (!TextUtils.isEmpty(data)) { suggestion.nickname = data; } } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) { long dataId = mDataCursor.getLong(DataQuery.ID); long photoId = mDataCursor.getLong(DataQuery.PHOTO_ID); if (dataId == photoId && !mDataCursor.isNull(DataQuery.PHOTO)) { suggestion.photo = mDataCursor.getBlob(DataQuery.PHOTO); } } } } return list; } public boolean containsRawContact(Suggestion suggestion, long rawContactId) { if (suggestion.rawContacts != null) { int count = suggestion.rawContacts.size(); for (int i = 0; i < count; i++) { if (suggestion.rawContacts.get(i).rawContactId == rawContactId) { return true; } } } return false; } }