blob: 0861d92a2a8c7a3f57035e6ff4db94c0c76e5d04 [file] [log] [blame]
/*
* 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;
}
}