Home | History | Annotate | Download | only in editor
      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 = new AggregationSuggestions.Builder()
    222                 .setLimit(MAX_SUGGESTION_COUNT)
    223                 .setContactId(mContactId);
    224 
    225         if (nameSb.length() != 0) {
    226             builder.addNameParameter(nameSb.toString());
    227         }
    228 
    229         if (phoneticNameSb.length() != 0) {
    230             builder.addNameParameter(phoneticNameSb.toString());
    231         }
    232 
    233         return builder.build();
    234     }
    235 
    236     private void appendValue(StringBuilder sb, ValuesDelta values, String column) {
    237         String value = values.getAsString(column);
    238         if (!TextUtils.isEmpty(value)) {
    239             if (sb.length() > 0) {
    240                 sb.append(' ');
    241             }
    242             sb.append(value);
    243         }
    244     }
    245 
    246     protected void handleMessage(Message msg) {
    247         switch (msg.what) {
    248             case MESSAGE_RESET:
    249                 mSuggestedContactIds = new long[0];
    250                 break;
    251             case MESSAGE_NAME_CHANGE:
    252                 loadAggregationSuggestions((Uri) msg.obj);
    253                 break;
    254         }
    255     }
    256 
    257     private static final class DataQuery {
    258 
    259         public static final String SELECTION_PREFIX =
    260                 Data.MIMETYPE + " IN ('"
    261                     + Phone.CONTENT_ITEM_TYPE + "','"
    262                     + Email.CONTENT_ITEM_TYPE + "','"
    263                     + StructuredName.CONTENT_ITEM_TYPE + "','"
    264                     + Nickname.CONTENT_ITEM_TYPE + "','"
    265                     + Photo.CONTENT_ITEM_TYPE + "')"
    266                 + " AND " + Data.CONTACT_ID + " IN (";
    267 
    268         public static final String[] COLUMNS = {
    269             Data._ID,
    270             Data.CONTACT_ID,
    271             Data.LOOKUP_KEY,
    272             Data.PHOTO_ID,
    273             Data.DISPLAY_NAME,
    274             Data.RAW_CONTACT_ID,
    275             Data.MIMETYPE,
    276             Data.DATA1,
    277             Data.IS_SUPER_PRIMARY,
    278             Photo.PHOTO,
    279             RawContacts.ACCOUNT_TYPE,
    280             RawContacts.ACCOUNT_NAME,
    281             RawContacts.DATA_SET
    282         };
    283 
    284         public static final int ID = 0;
    285         public static final int CONTACT_ID = 1;
    286         public static final int LOOKUP_KEY = 2;
    287         public static final int PHOTO_ID = 3;
    288         public static final int DISPLAY_NAME = 4;
    289         public static final int RAW_CONTACT_ID = 5;
    290         public static final int MIMETYPE = 6;
    291         public static final int DATA1 = 7;
    292         public static final int IS_SUPERPRIMARY = 8;
    293         public static final int PHOTO = 9;
    294         public static final int ACCOUNT_TYPE = 10;
    295         public static final int ACCOUNT_NAME = 11;
    296         public static final int DATA_SET = 12;
    297     }
    298 
    299     private void loadAggregationSuggestions(Uri uri) {
    300         ContentResolver contentResolver = mContext.getContentResolver();
    301         Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null);
    302         if (cursor == null) {
    303             return;
    304         }
    305         try {
    306             // If a new request is pending, chuck the result of the previous request
    307             if (getHandler().hasMessages(MESSAGE_NAME_CHANGE)) {
    308                 return;
    309             }
    310 
    311             boolean changed = updateSuggestedContactIds(cursor);
    312             if (!changed) {
    313                 return;
    314             }
    315 
    316             StringBuilder sb = new StringBuilder(DataQuery.SELECTION_PREFIX);
    317             int count = mSuggestedContactIds.length;
    318             for (int i = 0; i < count; i++) {
    319                 if (i > 0) {
    320                     sb.append(',');
    321                 }
    322                 sb.append(mSuggestedContactIds[i]);
    323             }
    324             sb.append(')');
    325             sb.toString();
    326 
    327             Cursor dataCursor = contentResolver.query(Data.CONTENT_URI,
    328                     DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID);
    329             if (dataCursor != null) {
    330                 mMainHandler.sendMessage(mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor));
    331             }
    332         } finally {
    333             cursor.close();
    334         }
    335     }
    336 
    337     private boolean updateSuggestedContactIds(final Cursor cursor) {
    338         final int count = cursor.getCount();
    339         boolean changed = count != mSuggestedContactIds.length;
    340         final ArrayList<Long> newIds = new ArrayList<Long>(count);
    341         while (cursor.moveToNext()) {
    342             final long contactId = cursor.getLong(0);
    343             if (!changed &&
    344                     Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) {
    345                 changed = true;
    346             }
    347             newIds.add(contactId);
    348         }
    349 
    350         if (changed) {
    351             mSuggestedContactIds = new long[newIds.size()];
    352             int i = 0;
    353             for (final Long newId : newIds) {
    354                 mSuggestedContactIds[i++] = newId;
    355             }
    356             Arrays.sort(mSuggestedContactIds);
    357         }
    358 
    359         return changed;
    360     }
    361 
    362     protected void deliverNotification(Cursor dataCursor) {
    363         if (mDataCursor != null) {
    364             mDataCursor.close();
    365         }
    366         mDataCursor = dataCursor;
    367         if (mListener != null) {
    368             mListener.onAggregationSuggestionChange();
    369         }
    370     }
    371 
    372     public int getSuggestedContactCount() {
    373         return mDataCursor != null ? mDataCursor.getCount() : 0;
    374     }
    375 
    376     public List<Suggestion> getSuggestions() {
    377         ArrayList<Suggestion> list = Lists.newArrayList();
    378         if (mDataCursor != null) {
    379             Suggestion suggestion = null;
    380             long currentContactId = -1;
    381             mDataCursor.moveToPosition(-1);
    382             while (mDataCursor.moveToNext()) {
    383                 long contactId = mDataCursor.getLong(DataQuery.CONTACT_ID);
    384                 if (contactId != currentContactId) {
    385                     suggestion = new Suggestion();
    386                     suggestion.contactId = contactId;
    387                     suggestion.name = mDataCursor.getString(DataQuery.DISPLAY_NAME);
    388                     suggestion.lookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY);
    389                     suggestion.rawContacts = Lists.newArrayList();
    390                     list.add(suggestion);
    391                     currentContactId = contactId;
    392                 }
    393 
    394                 long rawContactId = mDataCursor.getLong(DataQuery.RAW_CONTACT_ID);
    395                 if (!containsRawContact(suggestion, rawContactId)) {
    396                     RawContact rawContact = new RawContact();
    397                     rawContact.rawContactId = rawContactId;
    398                     rawContact.accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME);
    399                     rawContact.accountType = mDataCursor.getString(DataQuery.ACCOUNT_TYPE);
    400                     rawContact.dataSet = mDataCursor.getString(DataQuery.DATA_SET);
    401                     suggestion.rawContacts.add(rawContact);
    402                 }
    403 
    404                 String mimetype = mDataCursor.getString(DataQuery.MIMETYPE);
    405                 if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
    406                     String data = mDataCursor.getString(DataQuery.DATA1);
    407                     int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
    408                     if (!TextUtils.isEmpty(data)
    409                             && (superprimary != 0 || suggestion.phoneNumber == null)) {
    410                         suggestion.phoneNumber = data;
    411                     }
    412                 } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
    413                     String data = mDataCursor.getString(DataQuery.DATA1);
    414                     int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
    415                     if (!TextUtils.isEmpty(data)
    416                             && (superprimary != 0 || suggestion.emailAddress == null)) {
    417                         suggestion.emailAddress = data;
    418                     }
    419                 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) {
    420                     String data = mDataCursor.getString(DataQuery.DATA1);
    421                     if (!TextUtils.isEmpty(data)) {
    422                         suggestion.nickname = data;
    423                     }
    424                 } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
    425                     long dataId = mDataCursor.getLong(DataQuery.ID);
    426                     long photoId = mDataCursor.getLong(DataQuery.PHOTO_ID);
    427                     if (dataId == photoId && !mDataCursor.isNull(DataQuery.PHOTO)) {
    428                         suggestion.photo = mDataCursor.getBlob(DataQuery.PHOTO);
    429                     }
    430                 }
    431             }
    432         }
    433         return list;
    434     }
    435 
    436     public boolean containsRawContact(Suggestion suggestion, long rawContactId) {
    437         if (suggestion.rawContacts != null) {
    438             int count = suggestion.rawContacts.size();
    439             for (int i = 0; i < count; i++) {
    440                 if (suggestion.rawContacts.get(i).rawContactId == rawContactId) {
    441                     return true;
    442                 }
    443             }
    444         }
    445         return false;
    446     }
    447 }
    448