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.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.common.model.ValuesDelta;
     42 import com.android.contacts.compat.AggregationSuggestionsCompat;
     43 import com.google.common.collect.Lists;
     44 
     45 import java.util.ArrayList;
     46 import java.util.Arrays;
     47 import java.util.List;
     48 
     49 /**
     50  * Runs asynchronous queries to obtain aggregation suggestions in the as-you-type mode.
     51  */
     52 public class AggregationSuggestionEngine extends HandlerThread {
     53     public static final String TAG = "AggregationSuggestionEngine";
     54 
     55     public interface Listener {
     56         void onAggregationSuggestionChange();
     57     }
     58 
     59     public static final class RawContact {
     60         public long rawContactId;
     61         public String accountType;
     62         public String accountName;
     63         public String dataSet;
     64 
     65         @Override
     66         public String toString() {
     67             return "ID: " + rawContactId + " account: " + accountType + "/" + accountName
     68                     + " dataSet: " + dataSet;
     69         }
     70     }
     71 
     72     public static final class Suggestion {
     73 
     74         public long contactId;
     75         public long photoId;
     76         public String lookupKey;
     77         public String name;
     78         public String phoneNumber;
     79         public String emailAddress;
     80         public String nickname;
     81         public byte[] photo;
     82         public List<RawContact> rawContacts;
     83 
     84         @Override
     85         public String toString() {
     86             return "ID: " + contactId + " rawContacts: " + rawContacts + " name: " + name
     87             + " phone: " + phoneNumber + " email: " + emailAddress + " nickname: "
     88             + nickname + (photo != null ? " [has photo]" : "");
     89         }
     90     }
     91 
     92     private final class SuggestionContentObserver extends ContentObserver {
     93         private SuggestionContentObserver(Handler handler) {
     94             super(handler);
     95         }
     96 
     97         @Override
     98         public void onChange(boolean selfChange) {
     99             scheduleSuggestionLookup();
    100         }
    101     }
    102 
    103     private static final int MESSAGE_RESET = 0;
    104     private static final int MESSAGE_NAME_CHANGE = 1;
    105     private static final int MESSAGE_DATA_CURSOR = 2;
    106 
    107     private static final long SUGGESTION_LOOKUP_DELAY_MILLIS = 300;
    108 
    109     private final Context mContext;
    110 
    111     private long[] mSuggestedContactIds = new long[0];
    112 
    113     private Handler mMainHandler;
    114     private Handler mHandler;
    115     private long mContactId;
    116     private Listener mListener;
    117     private Cursor mDataCursor;
    118     private ContentObserver mContentObserver;
    119     private Uri mSuggestionsUri;
    120     private int mSuggestionsLimit = 3;
    121     private boolean mPruneInvisibleContacts = true;
    122 
    123     public AggregationSuggestionEngine(Context context) {
    124         super("AggregationSuggestions", Process.THREAD_PRIORITY_BACKGROUND);
    125         mContext = context.getApplicationContext();
    126         mMainHandler = new Handler() {
    127             @Override
    128             public void handleMessage(Message msg) {
    129                 AggregationSuggestionEngine.this.deliverNotification((Cursor) msg.obj);
    130             }
    131         };
    132     }
    133 
    134     protected Handler getHandler() {
    135         if (mHandler == null) {
    136             mHandler = new Handler(getLooper()) {
    137                 @Override
    138                 public void handleMessage(Message msg) {
    139                     AggregationSuggestionEngine.this.handleMessage(msg);
    140                 }
    141             };
    142         }
    143         return mHandler;
    144     }
    145 
    146     public void setContactId(long contactId) {
    147         if (contactId != mContactId) {
    148             mContactId = contactId;
    149             reset();
    150         }
    151     }
    152 
    153     public void setSuggestionsLimit(int suggestionsLimit) {
    154         mSuggestionsLimit = suggestionsLimit;
    155     }
    156 
    157     public void setPruneInvisibleContacts (boolean pruneInvisibleContacts) {
    158         mPruneInvisibleContacts = pruneInvisibleContacts;
    159     }
    160 
    161     public void setListener(Listener listener) {
    162         mListener = listener;
    163     }
    164 
    165     @Override
    166     public boolean quit() {
    167         if (mDataCursor != null) {
    168             mDataCursor.close();
    169         }
    170         mDataCursor = null;
    171         if (mContentObserver != null) {
    172             mContext.getContentResolver().unregisterContentObserver(mContentObserver);
    173             mContentObserver = null;
    174         }
    175         return super.quit();
    176     }
    177 
    178     public void reset() {
    179         Handler handler = getHandler();
    180         handler.removeMessages(MESSAGE_NAME_CHANGE);
    181         handler.sendEmptyMessage(MESSAGE_RESET);
    182     }
    183 
    184     public void onNameChange(ValuesDelta values) {
    185         mSuggestionsUri = buildAggregationSuggestionUri(values);
    186         if (mSuggestionsUri != null) {
    187             if (mContentObserver == null) {
    188                 mContentObserver = new SuggestionContentObserver(getHandler());
    189                 mContext.getContentResolver().registerContentObserver(
    190                         Contacts.CONTENT_URI, true, mContentObserver);
    191             }
    192         } else if (mContentObserver != null) {
    193             mContext.getContentResolver().unregisterContentObserver(mContentObserver);
    194             mContentObserver = null;
    195         }
    196         scheduleSuggestionLookup();
    197     }
    198 
    199     protected void scheduleSuggestionLookup() {
    200         Handler handler = getHandler();
    201         handler.removeMessages(MESSAGE_NAME_CHANGE);
    202 
    203         if (mSuggestionsUri == null) {
    204             return;
    205         }
    206 
    207         Message msg = handler.obtainMessage(MESSAGE_NAME_CHANGE, mSuggestionsUri);
    208         handler.sendMessageDelayed(msg, SUGGESTION_LOOKUP_DELAY_MILLIS);
    209     }
    210 
    211     private Uri buildAggregationSuggestionUri(ValuesDelta values) {
    212         StringBuilder nameSb = new StringBuilder();
    213         appendValue(nameSb, values, StructuredName.PREFIX);
    214         appendValue(nameSb, values, StructuredName.GIVEN_NAME);
    215         appendValue(nameSb, values, StructuredName.MIDDLE_NAME);
    216         appendValue(nameSb, values, StructuredName.FAMILY_NAME);
    217         appendValue(nameSb, values, StructuredName.SUFFIX);
    218 
    219         if (nameSb.length() == 0) {
    220             appendValue(nameSb, values, StructuredName.DISPLAY_NAME);
    221         }
    222 
    223         StringBuilder phoneticNameSb = new StringBuilder();
    224         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_FAMILY_NAME);
    225         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_MIDDLE_NAME);
    226         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_GIVEN_NAME);
    227 
    228         if (nameSb.length() == 0 && phoneticNameSb.length() == 0) {
    229             return null;
    230         }
    231 
    232         // AggregationSuggestions.Builder() became visible in API level 23, so use it if applicable.
    233         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    234             final Builder uriBuilder = new AggregationSuggestions.Builder()
    235                     .setLimit(mSuggestionsLimit)
    236                     .setContactId(mContactId);
    237             if (nameSb.length() != 0) {
    238                 uriBuilder.addNameParameter(nameSb.toString());
    239             }
    240             if (phoneticNameSb.length() != 0) {
    241                 uriBuilder.addNameParameter(phoneticNameSb.toString());
    242             }
    243             return uriBuilder.build();
    244         }
    245 
    246         // For previous SDKs, use the backup plan.
    247         final AggregationSuggestionsCompat.Builder uriBuilder =
    248                 new AggregationSuggestionsCompat.Builder()
    249                 .setLimit(mSuggestionsLimit)
    250                 .setContactId(mContactId);
    251         if (nameSb.length() != 0) {
    252             uriBuilder.addNameParameter(nameSb.toString());
    253         }
    254         if (phoneticNameSb.length() != 0) {
    255             uriBuilder.addNameParameter(phoneticNameSb.toString());
    256         }
    257         return uriBuilder.build();
    258     }
    259 
    260     private void appendValue(StringBuilder sb, ValuesDelta values, String column) {
    261         String value = values.getAsString(column);
    262         if (!TextUtils.isEmpty(value)) {
    263             if (sb.length() > 0) {
    264                 sb.append(' ');
    265             }
    266             sb.append(value);
    267         }
    268     }
    269 
    270     protected void handleMessage(Message msg) {
    271         switch (msg.what) {
    272             case MESSAGE_RESET:
    273                 mSuggestedContactIds = new long[0];
    274                 break;
    275             case MESSAGE_NAME_CHANGE:
    276                 loadAggregationSuggestions((Uri) msg.obj);
    277                 break;
    278         }
    279     }
    280 
    281     private static final class DataQuery {
    282 
    283         public static final String SELECTION_PREFIX =
    284                 Data.MIMETYPE + " IN ('"
    285                     + Phone.CONTENT_ITEM_TYPE + "','"
    286                     + Email.CONTENT_ITEM_TYPE + "','"
    287                     + StructuredName.CONTENT_ITEM_TYPE + "','"
    288                     + Nickname.CONTENT_ITEM_TYPE + "','"
    289                     + Photo.CONTENT_ITEM_TYPE + "')"
    290                 + " AND " + Data.CONTACT_ID + " IN (";
    291 
    292         public static final String[] COLUMNS = {
    293             Data._ID,
    294             Data.CONTACT_ID,
    295             Data.LOOKUP_KEY,
    296             Data.PHOTO_ID,
    297             Data.DISPLAY_NAME,
    298             Data.RAW_CONTACT_ID,
    299             Data.MIMETYPE,
    300             Data.DATA1,
    301             Data.IS_SUPER_PRIMARY,
    302             Photo.PHOTO,
    303             RawContacts.ACCOUNT_TYPE,
    304             RawContacts.ACCOUNT_NAME,
    305             RawContacts.DATA_SET
    306         };
    307 
    308         public static final int ID = 0;
    309         public static final int CONTACT_ID = 1;
    310         public static final int LOOKUP_KEY = 2;
    311         public static final int PHOTO_ID = 3;
    312         public static final int DISPLAY_NAME = 4;
    313         public static final int RAW_CONTACT_ID = 5;
    314         public static final int MIMETYPE = 6;
    315         public static final int DATA1 = 7;
    316         public static final int IS_SUPERPRIMARY = 8;
    317         public static final int PHOTO = 9;
    318         public static final int ACCOUNT_TYPE = 10;
    319         public static final int ACCOUNT_NAME = 11;
    320         public static final int DATA_SET = 12;
    321     }
    322 
    323     private void loadAggregationSuggestions(Uri uri) {
    324         ContentResolver contentResolver = mContext.getContentResolver();
    325         Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null);
    326         if (cursor == null) {
    327             return;
    328         }
    329         try {
    330             // If a new request is pending, chuck the result of the previous request
    331             if (getHandler().hasMessages(MESSAGE_NAME_CHANGE)) {
    332                 return;
    333             }
    334 
    335             boolean changed = updateSuggestedContactIds(cursor);
    336             if (!changed) {
    337                 return;
    338             }
    339 
    340             StringBuilder sb = new StringBuilder(DataQuery.SELECTION_PREFIX);
    341             int count = mSuggestedContactIds.length;
    342             for (int i = 0; i < count; i++) {
    343                 if (i > 0) {
    344                     sb.append(',');
    345                 }
    346                 sb.append(mSuggestedContactIds[i]);
    347             }
    348             sb.append(')');
    349             sb.toString();
    350 
    351             Cursor dataCursor = contentResolver.query(Data.CONTENT_URI,
    352                     DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID);
    353             if (dataCursor != null) {
    354                 mMainHandler.sendMessage(mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor));
    355             }
    356         } finally {
    357             cursor.close();
    358         }
    359     }
    360 
    361     private boolean updateSuggestedContactIds(final Cursor cursor) {
    362         final int count = cursor.getCount();
    363         boolean changed = count != mSuggestedContactIds.length;
    364         final ArrayList<Long> newIds = new ArrayList<Long>(count);
    365         while (cursor.moveToNext()) {
    366             final long contactId = cursor.getLong(0);
    367             if (!changed &&
    368                     Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) {
    369                 changed = true;
    370             }
    371             newIds.add(contactId);
    372         }
    373 
    374         if (changed) {
    375             mSuggestedContactIds = new long[newIds.size()];
    376             int i = 0;
    377             for (final Long newId : newIds) {
    378                 mSuggestedContactIds[i++] = newId;
    379             }
    380             Arrays.sort(mSuggestedContactIds);
    381         }
    382 
    383         return changed;
    384     }
    385 
    386     protected void deliverNotification(Cursor dataCursor) {
    387         if (mDataCursor != null) {
    388             mDataCursor.close();
    389         }
    390         mDataCursor = dataCursor;
    391         if (mListener != null) {
    392             mListener.onAggregationSuggestionChange();
    393         }
    394     }
    395 
    396     public int getSuggestedContactCount() {
    397         return mDataCursor != null ? mDataCursor.getCount() : 0;
    398     }
    399 
    400     public List<Suggestion> getSuggestions() {
    401         final ArrayList<Long> visibleContacts = new ArrayList<>();
    402         if (mPruneInvisibleContacts) {
    403             final Uri contactFilterUri = Data.CONTENT_URI.buildUpon()
    404                     .appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true")
    405                     .build();
    406             final ContentResolver contentResolver = mContext.getContentResolver();
    407             final Cursor contactCursor = contentResolver.query(contactFilterUri,
    408                     new String[]{Data.CONTACT_ID}, null, null, null);
    409             try {
    410                 if (contactCursor != null) {
    411                     while (contactCursor.moveToNext()) {
    412                         final long contactId = contactCursor.getLong(0);
    413                         visibleContacts.add(contactId);
    414                     }
    415                 }
    416             } finally {
    417                 contactCursor.close();
    418             }
    419 
    420         }
    421 
    422         ArrayList<Suggestion> list = Lists.newArrayList();
    423         if (mDataCursor != null) {
    424             Suggestion suggestion = null;
    425             long currentContactId = -1;
    426             mDataCursor.moveToPosition(-1);
    427             while (mDataCursor.moveToNext()) {
    428                 long contactId = mDataCursor.getLong(DataQuery.CONTACT_ID);
    429                 if (mPruneInvisibleContacts && !visibleContacts.contains(contactId)) {
    430                     continue;
    431                 }
    432                 if (contactId != currentContactId) {
    433                     suggestion = new Suggestion();
    434                     suggestion.contactId = contactId;
    435                     suggestion.name = mDataCursor.getString(DataQuery.DISPLAY_NAME);
    436                     suggestion.lookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY);
    437                     suggestion.rawContacts = Lists.newArrayList();
    438                     list.add(suggestion);
    439                     currentContactId = contactId;
    440                 }
    441 
    442                 long rawContactId = mDataCursor.getLong(DataQuery.RAW_CONTACT_ID);
    443                 if (!containsRawContact(suggestion, rawContactId)) {
    444                     RawContact rawContact = new RawContact();
    445                     rawContact.rawContactId = rawContactId;
    446                     rawContact.accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME);
    447                     rawContact.accountType = mDataCursor.getString(DataQuery.ACCOUNT_TYPE);
    448                     rawContact.dataSet = mDataCursor.getString(DataQuery.DATA_SET);
    449                     suggestion.rawContacts.add(rawContact);
    450                 }
    451 
    452                 String mimetype = mDataCursor.getString(DataQuery.MIMETYPE);
    453                 if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
    454                     String data = mDataCursor.getString(DataQuery.DATA1);
    455                     int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
    456                     if (!TextUtils.isEmpty(data)
    457                             && (superprimary != 0 || suggestion.phoneNumber == null)) {
    458                         suggestion.phoneNumber = data;
    459                     }
    460                 } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
    461                     String data = mDataCursor.getString(DataQuery.DATA1);
    462                     int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
    463                     if (!TextUtils.isEmpty(data)
    464                             && (superprimary != 0 || suggestion.emailAddress == null)) {
    465                         suggestion.emailAddress = data;
    466                     }
    467                 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) {
    468                     String data = mDataCursor.getString(DataQuery.DATA1);
    469                     if (!TextUtils.isEmpty(data)) {
    470                         suggestion.nickname = data;
    471                     }
    472                 } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
    473                     long dataId = mDataCursor.getLong(DataQuery.ID);
    474                     long photoId = mDataCursor.getLong(DataQuery.PHOTO_ID);
    475                     if (dataId == photoId && !mDataCursor.isNull(DataQuery.PHOTO)) {
    476                         suggestion.photo = mDataCursor.getBlob(DataQuery.PHOTO);
    477                         suggestion.photoId = photoId;
    478                     }
    479                 }
    480             }
    481         }
    482         return list;
    483     }
    484 
    485     public boolean containsRawContact(Suggestion suggestion, long rawContactId) {
    486         if (suggestion.rawContacts != null) {
    487             int count = suggestion.rawContacts.size();
    488             for (int i = 0; i < count; i++) {
    489                 if (suggestion.rawContacts.get(i).rawContactId == rawContactId) {
    490                     return true;
    491                 }
    492             }
    493         }
    494         return false;
    495     }
    496 }
    497