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