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.compat.AggregationSuggestionsCompat;
     42 import com.android.contacts.model.ValuesDelta;
     43 import com.android.contacts.model.account.AccountWithDataSet;
     44 
     45 import com.google.common.base.MoreObjects;
     46 import com.google.common.collect.Lists;
     47 
     48 import java.util.ArrayList;
     49 import java.util.Arrays;
     50 import java.util.List;
     51 
     52 /**
     53  * Runs asynchronous queries to obtain aggregation suggestions in the as-you-type mode.
     54  */
     55 public class AggregationSuggestionEngine extends HandlerThread {
     56     public interface Listener {
     57         void onAggregationSuggestionChange();
     58     }
     59 
     60     public static final class Suggestion {
     61         public long contactId;
     62         public String contactLookupKey;
     63         public long rawContactId;
     64         public long photoId = -1;
     65         public String name;
     66         public String phoneNumber;
     67         public String emailAddress;
     68         public String nickname;
     69 
     70         @Override
     71         public String toString() {
     72             return MoreObjects.toStringHelper(Suggestion.class)
     73                     .add("contactId", contactId)
     74                     .add("contactLookupKey", contactLookupKey)
     75                     .add("rawContactId", rawContactId)
     76                     .add("photoId", photoId)
     77                     .add("name", name)
     78                     .add("phoneNumber", phoneNumber)
     79                     .add("emailAddress", emailAddress)
     80                     .add("nickname", nickname)
     81                     .toString();
     82         }
     83     }
     84 
     85     private final class SuggestionContentObserver extends ContentObserver {
     86         private SuggestionContentObserver(Handler handler) {
     87             super(handler);
     88         }
     89 
     90         @Override
     91         public void onChange(boolean selfChange) {
     92             scheduleSuggestionLookup();
     93         }
     94     }
     95 
     96     private static final int MESSAGE_RESET = 0;
     97     private static final int MESSAGE_NAME_CHANGE = 1;
     98     private static final int MESSAGE_DATA_CURSOR = 2;
     99 
    100     private static final long SUGGESTION_LOOKUP_DELAY_MILLIS = 300;
    101 
    102     private static final int SUGGESTIONS_LIMIT = 3;
    103 
    104     private final Context mContext;
    105 
    106     private long[] mSuggestedContactIds = new long[0];
    107     private Handler mMainHandler;
    108     private Handler mHandler;
    109     private long mContactId;
    110     private AccountWithDataSet mAccountFilter;
    111     private Listener mListener;
    112     private Cursor mDataCursor;
    113     private ContentObserver mContentObserver;
    114     private Uri mSuggestionsUri;
    115 
    116     public AggregationSuggestionEngine(Context context) {
    117         super("AggregationSuggestions", Process.THREAD_PRIORITY_BACKGROUND);
    118         mContext = context.getApplicationContext();
    119         mMainHandler = new Handler() {
    120             @Override
    121             public void handleMessage(Message msg) {
    122                 AggregationSuggestionEngine.this.deliverNotification((Cursor) msg.obj);
    123             }
    124         };
    125     }
    126 
    127     protected Handler getHandler() {
    128         if (mHandler == null) {
    129             mHandler = new Handler(getLooper()) {
    130                 @Override
    131                 public void handleMessage(Message msg) {
    132                     AggregationSuggestionEngine.this.handleMessage(msg);
    133                 }
    134             };
    135         }
    136         return mHandler;
    137     }
    138 
    139     public void setContactId(long contactId) {
    140         if (contactId != mContactId) {
    141             mContactId = contactId;
    142             reset();
    143         }
    144     }
    145 
    146     public void setAccountFilter(AccountWithDataSet account) {
    147         mAccountFilter = account;
    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         StringBuilder phoneticNameSb = new StringBuilder();
    209         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_FAMILY_NAME);
    210         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_MIDDLE_NAME);
    211         appendValue(phoneticNameSb, values, StructuredName.PHONETIC_GIVEN_NAME);
    212 
    213         if (nameSb.length() == 0 && phoneticNameSb.length() == 0) {
    214             return null;
    215         }
    216 
    217         // AggregationSuggestions.Builder() became visible in API level 23, so use it if applicable.
    218         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    219             final Builder uriBuilder = new AggregationSuggestions.Builder()
    220                     .setLimit(SUGGESTIONS_LIMIT)
    221                     .setContactId(mContactId);
    222             if (nameSb.length() != 0) {
    223                 uriBuilder.addNameParameter(nameSb.toString());
    224             }
    225             if (phoneticNameSb.length() != 0) {
    226                 uriBuilder.addNameParameter(phoneticNameSb.toString());
    227             }
    228             return uriBuilder.build();
    229         }
    230 
    231         // For previous SDKs, use the backup plan.
    232         final AggregationSuggestionsCompat.Builder uriBuilder =
    233                 new AggregationSuggestionsCompat.Builder()
    234                 .setLimit(SUGGESTIONS_LIMIT)
    235                 .setContactId(mContactId);
    236         if (nameSb.length() != 0) {
    237             uriBuilder.addNameParameter(nameSb.toString());
    238         }
    239         if (phoneticNameSb.length() != 0) {
    240             uriBuilder.addNameParameter(phoneticNameSb.toString());
    241         }
    242         return uriBuilder.build();
    243     }
    244 
    245     private void appendValue(StringBuilder sb, ValuesDelta values, String column) {
    246         String value = values.getAsString(column);
    247         if (!TextUtils.isEmpty(value)) {
    248             if (sb.length() > 0) {
    249                 sb.append(' ');
    250             }
    251             sb.append(value);
    252         }
    253     }
    254 
    255     protected void handleMessage(Message msg) {
    256         switch (msg.what) {
    257             case MESSAGE_RESET:
    258                 mSuggestedContactIds = new long[0];
    259                 break;
    260             case MESSAGE_NAME_CHANGE:
    261                 loadAggregationSuggestions((Uri) msg.obj);
    262                 break;
    263         }
    264     }
    265 
    266     private static final class DataQuery {
    267 
    268         public static final String SELECTION_PREFIX =
    269                 Data.MIMETYPE + " IN ('"
    270                         + Phone.CONTENT_ITEM_TYPE + "','"
    271                         + Email.CONTENT_ITEM_TYPE + "','"
    272                         + StructuredName.CONTENT_ITEM_TYPE + "','"
    273                         + Nickname.CONTENT_ITEM_TYPE + "','"
    274                         + Photo.CONTENT_ITEM_TYPE + "')"
    275                         + " AND " + Data.CONTACT_ID + " IN (";
    276 
    277         public static final String[] COLUMNS = {
    278                 Data.CONTACT_ID,
    279                 Data.LOOKUP_KEY,
    280                 Data.RAW_CONTACT_ID,
    281                 Data.MIMETYPE,
    282                 Data.DATA1,
    283                 Data.IS_SUPER_PRIMARY,
    284                 RawContacts.ACCOUNT_TYPE,
    285                 RawContacts.ACCOUNT_NAME,
    286                 RawContacts.DATA_SET,
    287                 Contacts.Photo._ID
    288         };
    289 
    290         public static final int CONTACT_ID = 0;
    291         public static final int LOOKUP_KEY = 1;
    292         public static final int RAW_CONTACT_ID = 2;
    293         public static final int MIMETYPE = 3;
    294         public static final int DATA1 = 4;
    295         public static final int IS_SUPERPRIMARY = 5;
    296         public static final int ACCOUNT_TYPE = 6;
    297         public static final int ACCOUNT_NAME = 7;
    298         public static final int DATA_SET = 8;
    299         public static final int PHOTO_ID = 9;
    300     }
    301 
    302     private void loadAggregationSuggestions(Uri uri) {
    303         ContentResolver contentResolver = mContext.getContentResolver();
    304         Cursor cursor = contentResolver.query(uri, new String[]{Contacts._ID}, null, null, null);
    305         if (cursor == null) {
    306             return;
    307         }
    308         try {
    309             // If a new request is pending, chuck the result of the previous request
    310             if (getHandler().hasMessages(MESSAGE_NAME_CHANGE)) {
    311                 return;
    312             }
    313 
    314             boolean changed = updateSuggestedContactIds(cursor);
    315             if (!changed) {
    316                 return;
    317             }
    318 
    319             StringBuilder sb = new StringBuilder(DataQuery.SELECTION_PREFIX);
    320             int count = mSuggestedContactIds.length;
    321             for (int i = 0; i < count; i++) {
    322                 if (i > 0) {
    323                     sb.append(',');
    324                 }
    325                 sb.append(mSuggestedContactIds[i]);
    326             }
    327             sb.append(')');
    328 
    329             Cursor dataCursor = contentResolver.query(Data.CONTENT_URI,
    330                     DataQuery.COLUMNS, sb.toString(), null, Data.CONTACT_ID);
    331             if (dataCursor != null) {
    332                 mMainHandler.sendMessage(
    333                         mMainHandler.obtainMessage(MESSAGE_DATA_CURSOR, dataCursor));
    334             }
    335         } finally {
    336             cursor.close();
    337         }
    338     }
    339 
    340     private boolean updateSuggestedContactIds(final Cursor cursor) {
    341         final int count = cursor.getCount();
    342         boolean changed = count != mSuggestedContactIds.length;
    343         final ArrayList<Long> newIds = new ArrayList<Long>(count);
    344         while (cursor.moveToNext()) {
    345             final long contactId = cursor.getLong(0);
    346             if (!changed && Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) {
    347                 changed = true;
    348             }
    349             newIds.add(contactId);
    350         }
    351 
    352         if (changed) {
    353             mSuggestedContactIds = new long[newIds.size()];
    354             int i = 0;
    355             for (final Long newId : newIds) {
    356                 mSuggestedContactIds[i++] = newId;
    357             }
    358             Arrays.sort(mSuggestedContactIds);
    359         }
    360 
    361         return changed;
    362     }
    363 
    364     protected void deliverNotification(Cursor dataCursor) {
    365         if (mDataCursor != null) {
    366             mDataCursor.close();
    367         }
    368         mDataCursor = dataCursor;
    369         if (mListener != null) {
    370             mListener.onAggregationSuggestionChange();
    371         }
    372     }
    373 
    374     public int getSuggestedContactCount() {
    375         return mDataCursor != null ? mDataCursor.getCount() : 0;
    376     }
    377 
    378     public List<Suggestion> getSuggestions() {
    379         final ArrayList<Suggestion> list = Lists.newArrayList();
    380 
    381         if (mDataCursor != null && mAccountFilter != null) {
    382             Suggestion suggestion = null;
    383             long currentRawContactId = -1;
    384             mDataCursor.moveToPosition(-1);
    385             while (mDataCursor.moveToNext()) {
    386                 final long rawContactId = mDataCursor.getLong(DataQuery.RAW_CONTACT_ID);
    387                 if (rawContactId != currentRawContactId) {
    388                     suggestion = new Suggestion();
    389                     suggestion.rawContactId = rawContactId;
    390                     suggestion.contactId = mDataCursor.getLong(DataQuery.CONTACT_ID);
    391                     suggestion.contactLookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY);
    392                     final String accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME);
    393                     final String accountType = mDataCursor.getString(DataQuery.ACCOUNT_TYPE);
    394                     final String dataSet = mDataCursor.getString(DataQuery.DATA_SET);
    395                     final AccountWithDataSet account = new AccountWithDataSet(
    396                             accountName, accountType, dataSet);
    397                     if (mAccountFilter.equals(account)) {
    398                         list.add(suggestion);
    399                     }
    400                     currentRawContactId = rawContactId;
    401                 }
    402 
    403                 final String mimetype = mDataCursor.getString(DataQuery.MIMETYPE);
    404                 if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
    405                     final String data = mDataCursor.getString(DataQuery.DATA1);
    406                     int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
    407                     if (!TextUtils.isEmpty(data)
    408                             && (superprimary != 0 || suggestion.phoneNumber == null)) {
    409                         suggestion.phoneNumber = data;
    410                     }
    411                 } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
    412                     final String data = mDataCursor.getString(DataQuery.DATA1);
    413                     int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
    414                     if (!TextUtils.isEmpty(data)
    415                             && (superprimary != 0 || suggestion.emailAddress == null)) {
    416                         suggestion.emailAddress = data;
    417                     }
    418                 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) {
    419                     final String data = mDataCursor.getString(DataQuery.DATA1);
    420                     if (!TextUtils.isEmpty(data)) {
    421                         suggestion.nickname = data;
    422                     }
    423                 } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
    424                     // DATA1 stores the display name for the raw contact.
    425                     final String data = mDataCursor.getString(DataQuery.DATA1);
    426                     if (!TextUtils.isEmpty(data) && suggestion.name == null) {
    427                         suggestion.name = data;
    428                     }
    429                 } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
    430                     final Long id = mDataCursor.getLong(DataQuery.PHOTO_ID);
    431                     if (suggestion.photoId == -1) {
    432                         suggestion.photoId = id;
    433                     }
    434                 }
    435             }
    436         }
    437         return list;
    438     }
    439 }
    440