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