GlobalSearchSupport.java revision b38ed2c5ffeb20efc677b4a9229db4a00603aa8d
1/*
2 * Copyright (C) 2009 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.providers.contacts;
18
19import com.android.internal.database.ArrayListCursor;
20import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
21import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
22import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
23import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
24import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
25
26import android.app.SearchManager;
27import android.content.ContentUris;
28import android.content.res.Resources;
29import android.database.Cursor;
30import android.database.sqlite.SQLiteDatabase;
31import android.net.Uri;
32import android.provider.Contacts.Intents;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.Data;
35import android.provider.ContactsContract.Presence;
36import android.provider.ContactsContract.RawContacts;
37import android.provider.ContactsContract.StatusUpdates;
38import android.provider.ContactsContract.CommonDataKinds.Email;
39import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
40import android.provider.ContactsContract.CommonDataKinds.Organization;
41import android.provider.ContactsContract.CommonDataKinds.Phone;
42import android.provider.ContactsContract.CommonDataKinds.StructuredName;
43import android.provider.ContactsContract.Contacts.Photo;
44import android.text.TextUtils;
45
46import java.util.ArrayList;
47import java.util.Collections;
48import java.util.Comparator;
49import java.util.HashMap;
50
51/**
52 * Support for global search integration for Contacts.
53 */
54public class GlobalSearchSupport {
55
56    private static final String[] SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS = {
57            "_id",
58            SearchManager.SUGGEST_COLUMN_TEXT_1,
59            SearchManager.SUGGEST_COLUMN_TEXT_2,
60            SearchManager.SUGGEST_COLUMN_ICON_1,
61            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
62            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
63            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
64    };
65
66    private static final String[] SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS = {
67            "_id",
68            SearchManager.SUGGEST_COLUMN_TEXT_1,
69            SearchManager.SUGGEST_COLUMN_TEXT_2,
70            SearchManager.SUGGEST_COLUMN_ICON_1,
71            SearchManager.SUGGEST_COLUMN_ICON_2,
72            SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
73            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
74    };
75
76    private interface SearchSuggestionQuery {
77        public static final String JOIN_RAW_CONTACTS =
78                " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) ";
79
80        public static final String JOIN_CONTACTS =
81                " JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
82
83        public static final String JOIN_MIMETYPES =
84                " JOIN mimetypes ON (data.mimetype_id = mimetypes._id AND mimetypes.mimetype IN ('"
85                + StructuredName.CONTENT_ITEM_TYPE + "','" + Email.CONTENT_ITEM_TYPE + "','"
86                + Phone.CONTENT_ITEM_TYPE + "','" + Organization.CONTENT_ITEM_TYPE + "','"
87                + GroupMembership.CONTENT_ITEM_TYPE + "')) ";
88
89        public static final String TABLE = "data " + JOIN_RAW_CONTACTS + JOIN_MIMETYPES
90                + JOIN_CONTACTS;
91
92        public static final String PRESENCE_SQL =
93                "(SELECT " + StatusUpdates.PRESENCE_STATUS +
94                " FROM " + Tables.AGGREGATED_PRESENCE +
95                " WHERE " + AggregatedPresenceColumns.CONTACT_ID
96                        + "=" + ContactsColumns.CONCRETE_ID + ")";
97
98        public static final String[] COLUMNS = {
99            ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID,
100            ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + Contacts.DISPLAY_NAME,
101            PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE,
102            DataColumns.CONCRETE_ID + " AS data_id",
103            MimetypesColumns.MIMETYPE,
104            Data.IS_SUPER_PRIMARY,
105            Organization.COMPANY,
106            Email.DATA,
107            Phone.NUMBER,
108            Contacts.PHOTO_ID,
109        };
110
111        public static final int CONTACT_ID = 0;
112        public static final int DISPLAY_NAME = 1;
113        public static final int PRESENCE_STATUS = 2;
114        public static final int DATA_ID = 3;
115        public static final int MIMETYPE = 4;
116        public static final int IS_SUPER_PRIMARY = 5;
117        public static final int ORGANIZATION = 6;
118        public static final int EMAIL = 7;
119        public static final int PHONE = 8;
120        public static final int PHOTO_ID = 9;
121    }
122
123    private static class SearchSuggestion {
124        String contactId;
125        boolean titleIsName;
126        String organization;
127        String email;
128        String phoneNumber;
129        Uri photoUri;
130        String normalizedName;
131        int presence = -1;
132        boolean processed;
133        String text1;
134        String text2;
135        String icon1;
136        String icon2;
137
138        public SearchSuggestion(long contactId) {
139            this.contactId = String.valueOf(contactId);
140        }
141
142        private void process() {
143            if (processed) {
144                return;
145            }
146
147            boolean hasOrganization = !TextUtils.isEmpty(organization);
148            boolean hasEmail = !TextUtils.isEmpty(email);
149            boolean hasPhone = !TextUtils.isEmpty(phoneNumber);
150
151            boolean titleIsOrganization = !titleIsName && hasOrganization;
152            boolean titleIsEmail = !titleIsName && !titleIsOrganization && hasEmail;
153            boolean titleIsPhone = !titleIsName && !titleIsOrganization && !titleIsEmail
154                    && hasPhone;
155
156            if (!titleIsOrganization && hasOrganization) {
157                text2 = organization;
158            } else if (!titleIsEmail && hasEmail) {
159                text2 = email;
160            } else if (!titleIsPhone && hasPhone) {
161                text2 = phoneNumber;
162            }
163
164            if (photoUri != null) {
165                icon1 = photoUri.toString();
166            } else {
167                icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
168            }
169
170            if (presence != -1) {
171                icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence));
172            }
173
174            processed = true;
175        }
176
177        public String getSortKey() {
178            if (normalizedName == null) {
179                process();
180                normalizedName = text1 == null ? "" : NameNormalizer.normalize(text1);
181            }
182            return normalizedName;
183        }
184
185        @SuppressWarnings({"unchecked"})
186        public ArrayList asList(String[] projection) {
187            process();
188
189            ArrayList<Object> list = new ArrayList<Object>();
190            if (projection == null) {
191                list.add(contactId);
192                list.add(text1);
193                list.add(text2);
194                list.add(icon1);
195                list.add(icon2);
196                list.add(contactId);
197                list.add(contactId);
198            } else {
199                for (int i = 0; i < projection.length; i++) {
200                    addColumnValue(list, projection[i]);
201                }
202            }
203            return list;
204        }
205
206        private void addColumnValue(ArrayList<Object> list, String column) {
207            if ("_id".equals(column)) {
208                list.add(contactId);
209            } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) {
210                list.add(text1);
211            } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) {
212                list.add(text2);
213            } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) {
214                list.add(icon1);
215            } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) {
216                list.add(icon2);
217            } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) {
218                list.add(contactId);
219            } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) {
220                list.add(contactId);
221            } else {
222                throw new IllegalArgumentException("Invalid column name: " + column);
223            }
224        }
225    }
226
227    private final ContactsProvider2 mContactsProvider;
228
229    public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
230        mContactsProvider = contactsProvider;
231    }
232
233    public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String limit) {
234        if (uri.getPathSegments().size() <= 1) {
235            return null;
236        }
237
238        final String searchClause = uri.getLastPathSegment();
239        if (TextUtils.isDigitsOnly(searchClause)) {
240            return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause);
241        } else {
242            return buildCursorForSearchSuggestionsBasedOnName(db, searchClause, limit);
243        }
244    }
245
246    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, long contactId, String[] projection) {
247        StringBuilder sb = new StringBuilder();
248        sb.append(mContactsProvider.getContactsRestrictions());
249        sb.append(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
250        return buildCursorForSearchSuggestions(db, sb.toString(), projection);
251    }
252
253    private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
254        Resources r = mContactsProvider.getContext().getResources();
255        String s;
256        int i;
257
258        ArrayList<Object> dialNumber = new ArrayList<Object>();
259        dialNumber.add(0);  // _id
260        s = r.getString(com.android.internal.R.string.dial_number_using, searchClause);
261        i = s.indexOf('\n');
262        if (i < 0) {
263            dialNumber.add(s);
264            dialNumber.add("");
265        } else {
266            dialNumber.add(s.substring(0, i));
267            dialNumber.add(s.substring(i + 1));
268        }
269        dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact));
270        dialNumber.add("tel:" + searchClause);
271        dialNumber.add(Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
272        dialNumber.add(null);
273
274        ArrayList<Object> createContact = new ArrayList<Object>();
275        createContact.add(1);  // _id
276        s = r.getString(com.android.internal.R.string.create_contact_using, searchClause);
277        i = s.indexOf('\n');
278        if (i < 0) {
279            createContact.add(s);
280            createContact.add("");
281        } else {
282            createContact.add(s.substring(0, i));
283            createContact.add(s.substring(i + 1));
284        }
285        createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact));
286        createContact.add("tel:" + searchClause);
287        createContact.add(Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
288        createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
289
290        @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
291        rows.add(dialNumber);
292        rows.add(createContact);
293
294        return new ArrayListCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS, rows);
295    }
296
297    private Cursor buildCursorForSearchSuggestionsBasedOnName(SQLiteDatabase db,
298            String searchClause, String limit) {
299
300        StringBuilder sb = new StringBuilder();
301        sb.append(mContactsProvider.getContactsRestrictions());
302        sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
303        mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause, limit);
304        sb.append(" AND " + Contacts.IN_VISIBLE_GROUP + "=1");
305
306        return buildCursorForSearchSuggestions(db, sb.toString(), null);
307    }
308
309    private Cursor buildCursorForSearchSuggestions(SQLiteDatabase db, String selection,
310            String[] projection) {
311        ArrayList<SearchSuggestion> suggestionList = new ArrayList<SearchSuggestion>();
312        HashMap<Long, SearchSuggestion> suggestionMap = new HashMap<Long, SearchSuggestion>();
313        Cursor c = db.query(true, SearchSuggestionQuery.TABLE,
314                SearchSuggestionQuery.COLUMNS, selection, null, null, null, null, null);
315        try {
316            while (c.moveToNext()) {
317
318                long contactId = c.getLong(SearchSuggestionQuery.CONTACT_ID);
319                SearchSuggestion suggestion = suggestionMap.get(contactId);
320                if (suggestion == null) {
321                    suggestion = new SearchSuggestion(contactId);
322                    suggestionList.add(suggestion);
323                    suggestionMap.put(contactId, suggestion);
324                }
325
326                boolean isSuperPrimary = c.getInt(SearchSuggestionQuery.IS_SUPER_PRIMARY) != 0;
327                suggestion.text1 = c.getString(SearchSuggestionQuery.DISPLAY_NAME);
328
329                if (!c.isNull(SearchSuggestionQuery.PRESENCE_STATUS)) {
330                    suggestion.presence = c.getInt(SearchSuggestionQuery.PRESENCE_STATUS);
331                }
332
333                String mimetype = c.getString(SearchSuggestionQuery.MIMETYPE);
334                if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
335                    suggestion.titleIsName = true;
336                } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) {
337                    if (isSuperPrimary || suggestion.organization == null) {
338                        suggestion.organization = c.getString(SearchSuggestionQuery.ORGANIZATION);
339                    }
340                } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
341                    if (isSuperPrimary || suggestion.email == null) {
342                        suggestion.email = c.getString(SearchSuggestionQuery.EMAIL);
343                    }
344                } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
345                    if (isSuperPrimary || suggestion.phoneNumber == null) {
346                        suggestion.phoneNumber = c.getString(SearchSuggestionQuery.PHONE);
347                    }
348                }
349
350                if (!c.isNull(SearchSuggestionQuery.PHOTO_ID)) {
351                    suggestion.photoUri = Uri.withAppendedPath(
352                            ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
353                            Photo.CONTENT_DIRECTORY);
354                }
355            }
356        } finally {
357            c.close();
358        }
359
360        Collections.sort(suggestionList, new Comparator<SearchSuggestion>() {
361            public int compare(SearchSuggestion row1, SearchSuggestion row2) {
362                return row1.getSortKey().compareTo(row2.getSortKey());
363            }
364        });
365
366        @SuppressWarnings({"unchecked"}) ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
367        for (int i = 0; i < suggestionList.size(); i++) {
368            rows.add(suggestionList.get(i).asList(projection));
369        }
370
371        return new ArrayListCursor(projection != null ? projection
372                : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS, rows);
373    }
374}
375