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.Handler;
25import android.os.HandlerThread;
26import android.os.Message;
27import android.os.Process;
28import android.provider.ContactsContract.CommonDataKinds.Email;
29import android.provider.ContactsContract.CommonDataKinds.Nickname;
30import android.provider.ContactsContract.CommonDataKinds.Phone;
31import android.provider.ContactsContract.CommonDataKinds.Photo;
32import android.provider.ContactsContract.CommonDataKinds.StructuredName;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.Contacts.AggregationSuggestions;
35import android.provider.ContactsContract.Contacts.AggregationSuggestions.Builder;
36import android.provider.ContactsContract.Data;
37import android.provider.ContactsContract.RawContacts;
38import android.text.TextUtils;
39
40import com.android.contacts.common.model.ValuesDelta;
41import com.google.common.collect.Lists;
42
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.List;
46
47/**
48 * Runs asynchronous queries to obtain aggregation suggestions in the as-you-type mode.
49 */
50public 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 = 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
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
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
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
338    private boolean updateSuggestedContactIds(final Cursor cursor) {
339        final int count = cursor.getCount();
340        boolean changed = count != mSuggestedContactIds.length;
341        final ArrayList<Long> newIds = new ArrayList<Long>(count);
342        while (cursor.moveToNext()) {
343            final long contactId = cursor.getLong(0);
344            if (!changed &&
345                    Arrays.binarySearch(mSuggestedContactIds, contactId) < 0) {
346                changed = true;
347            }
348            newIds.add(contactId);
349        }
350
351        if (changed) {
352            mSuggestedContactIds = new long[newIds.size()];
353            int i = 0;
354            for (final Long newId : newIds) {
355                mSuggestedContactIds[i++] = newId;
356            }
357            Arrays.sort(mSuggestedContactIds);
358        }
359
360        return changed;
361    }
362
363    protected void deliverNotification(Cursor dataCursor) {
364        if (mDataCursor != null) {
365            mDataCursor.close();
366        }
367        mDataCursor = dataCursor;
368        if (mListener != null) {
369            mListener.onAggregationSuggestionChange();
370        }
371    }
372
373    public int getSuggestedContactCount() {
374        return mDataCursor != null ? mDataCursor.getCount() : 0;
375    }
376
377    public List<Suggestion> getSuggestions() {
378        ArrayList<Suggestion> list = Lists.newArrayList();
379        if (mDataCursor != null) {
380            Suggestion suggestion = null;
381            long currentContactId = -1;
382            mDataCursor.moveToPosition(-1);
383            while (mDataCursor.moveToNext()) {
384                long contactId = mDataCursor.getLong(DataQuery.CONTACT_ID);
385                if (contactId != currentContactId) {
386                    suggestion = new Suggestion();
387                    suggestion.contactId = contactId;
388                    suggestion.name = mDataCursor.getString(DataQuery.DISPLAY_NAME);
389                    suggestion.lookupKey = mDataCursor.getString(DataQuery.LOOKUP_KEY);
390                    suggestion.rawContacts = Lists.newArrayList();
391                    list.add(suggestion);
392                    currentContactId = contactId;
393                }
394
395                long rawContactId = mDataCursor.getLong(DataQuery.RAW_CONTACT_ID);
396                if (!containsRawContact(suggestion, rawContactId)) {
397                    RawContact rawContact = new RawContact();
398                    rawContact.rawContactId = rawContactId;
399                    rawContact.accountName = mDataCursor.getString(DataQuery.ACCOUNT_NAME);
400                    rawContact.accountType = mDataCursor.getString(DataQuery.ACCOUNT_TYPE);
401                    rawContact.dataSet = mDataCursor.getString(DataQuery.DATA_SET);
402                    suggestion.rawContacts.add(rawContact);
403                }
404
405                String mimetype = mDataCursor.getString(DataQuery.MIMETYPE);
406                if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
407                    String data = mDataCursor.getString(DataQuery.DATA1);
408                    int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
409                    if (!TextUtils.isEmpty(data)
410                            && (superprimary != 0 || suggestion.phoneNumber == null)) {
411                        suggestion.phoneNumber = data;
412                    }
413                } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
414                    String data = mDataCursor.getString(DataQuery.DATA1);
415                    int superprimary = mDataCursor.getInt(DataQuery.IS_SUPERPRIMARY);
416                    if (!TextUtils.isEmpty(data)
417                            && (superprimary != 0 || suggestion.emailAddress == null)) {
418                        suggestion.emailAddress = data;
419                    }
420                } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) {
421                    String data = mDataCursor.getString(DataQuery.DATA1);
422                    if (!TextUtils.isEmpty(data)) {
423                        suggestion.nickname = data;
424                    }
425                } else if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
426                    long dataId = mDataCursor.getLong(DataQuery.ID);
427                    long photoId = mDataCursor.getLong(DataQuery.PHOTO_ID);
428                    if (dataId == photoId && !mDataCursor.isNull(DataQuery.PHOTO)) {
429                        suggestion.photo = mDataCursor.getBlob(DataQuery.PHOTO);
430                    }
431                }
432            }
433        }
434        return list;
435    }
436
437    public boolean containsRawContact(Suggestion suggestion, long rawContactId) {
438        if (suggestion.rawContacts != null) {
439            int count = suggestion.rawContacts.size();
440            for (int i = 0; i < count; i++) {
441                if (suggestion.rawContacts.get(i).rawContactId == rawContactId) {
442                    return true;
443                }
444            }
445        }
446        return false;
447    }
448}
449