GlobalSearchSupport.java revision 5bd028407806015c91d863ee2bbffbaaf1c200d8
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.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
20import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
21import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
22
23import android.app.SearchManager;
24import android.content.res.Resources;
25import android.database.Cursor;
26import android.database.MatrixCursor;
27import android.database.sqlite.SQLiteDatabase;
28import android.net.Uri;
29import android.provider.ContactsContract;
30import android.provider.ContactsContract.CommonDataKinds.Email;
31import android.provider.ContactsContract.CommonDataKinds.Organization;
32import android.provider.ContactsContract.CommonDataKinds.Phone;
33import android.provider.ContactsContract.Contacts;
34import android.provider.ContactsContract.Data;
35import android.provider.ContactsContract.SearchSnippetColumns;
36import android.provider.ContactsContract.StatusUpdates;
37import android.text.TextUtils;
38
39import java.util.ArrayList;
40
41/**
42 * Support for global search integration for Contacts.
43 */
44public class GlobalSearchSupport {
45
46    private static final String[] SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS = {
47            "_id",
48            SearchManager.SUGGEST_COLUMN_TEXT_1,
49            SearchManager.SUGGEST_COLUMN_TEXT_2,
50            SearchManager.SUGGEST_COLUMN_ICON_1,
51            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
52            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
53            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
54    };
55
56    private static final String[] SEARCH_SUGGESTIONS_BASED_ON_NAME_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_ICON_2,
62            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
63            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
64            SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
65    };
66
67    private static final char SNIPPET_START_MATCH = '\u0001';
68    private static final char SNIPPET_END_MATCH = '\u0001';
69    private static final String SNIPPET_ELLIPSIS = "\u2026";
70    private static final int SNIPPET_MAX_TOKENS = 5;
71
72    private static final String PRESENCE_SQL =
73        "(SELECT " + StatusUpdates.PRESENCE +
74        " FROM " + Tables.AGGREGATED_PRESENCE +
75        " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
76
77    // Current contacts - those contacted within the last 3 days (in seconds)
78    private static final long CURRENT_CONTACTS = 3 * 24 * 60 * 60;
79
80    // Recent contacts - those contacted within the last 30 days (in seconds)
81    private static final long RECENT_CONTACTS = 30 * 24 * 60 * 60;
82
83    private static final String TIME_SINCE_LAST_CONTACTED =
84            "(strftime('%s', 'now') - contacts." + Contacts.LAST_TIME_CONTACTED + "/1000)";
85
86    /*
87     * See {@link ContactsProvider2#EMAIL_FILTER_SORT_ORDER} for the discussion of this
88     * sorting order.
89     */
90    private static final String SORT_ORDER =
91        "(CASE WHEN contacts." + Contacts.STARRED + "=1 THEN 0 ELSE 1 END), "
92        + "(CASE WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + CURRENT_CONTACTS + " THEN 0 "
93        + " WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + RECENT_CONTACTS + " THEN 1 "
94        + " ELSE 2 END),"
95        + "contacts." + Contacts.TIMES_CONTACTED + " DESC, "
96        + "contacts." + Contacts.DISPLAY_NAME_PRIMARY + ", "
97        + "contacts." + Contacts._ID;
98
99    private static final String RECENTLY_CONTACTED =
100        TIME_SINCE_LAST_CONTACTED + " < " + RECENT_CONTACTS;
101
102    private static class SearchSuggestion {
103        long contactId;
104        String photoUri;
105        String lookupKey;
106        int presence = -1;
107        String text1;
108        String text2;
109        String icon1;
110        String icon2;
111        String filter;
112
113        @SuppressWarnings({"unchecked"})
114        public ArrayList asList(String[] projection) {
115            if (photoUri != null) {
116                icon1 = photoUri.toString();
117            } else {
118                icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
119            }
120
121            if (presence != -1) {
122                icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence));
123            }
124
125            ArrayList<Object> list = new ArrayList<Object>();
126            if (projection == null) {
127                list.add(contactId);
128                list.add(text1);
129                list.add(text2);
130                list.add(icon1);
131                list.add(icon2);
132                list.add(buildUri());
133                list.add(lookupKey);
134                list.add(filter);
135            } else {
136                for (int i = 0; i < projection.length; i++) {
137                    addColumnValue(list, projection[i]);
138                }
139            }
140            return list;
141        }
142
143        private void addColumnValue(ArrayList<Object> list, String column) {
144            if ("_id".equals(column)) {
145                list.add(contactId);
146            } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) {
147                list.add(text1);
148            } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) {
149                list.add(text2);
150            } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) {
151                list.add(icon1);
152            } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) {
153                list.add(icon2);
154            } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA.equals(column)) {
155                list.add(buildUri());
156            } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) {
157                list.add(lookupKey);
158            } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) {
159                list.add(lookupKey);
160            } else if (SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA.equals(column)) {
161                list.add(filter);
162            } else {
163                throw new IllegalArgumentException("Invalid column name: " + column);
164            }
165        }
166
167        private String buildUri() {
168            return Contacts.getLookupUri(contactId, lookupKey).toString();
169        }
170    }
171
172    private final ContactsProvider2 mContactsProvider;
173
174    @SuppressWarnings("all")
175    public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
176        mContactsProvider = contactsProvider;
177
178        // To ensure the data column position. This is dead code if properly configured.
179        if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
180                || Email.DATA != Data.DATA1) {
181            throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
182                    + " data is not in DATA1 column");
183        }
184    }
185
186    public Cursor handleSearchSuggestionsQuery(
187            SQLiteDatabase db, Uri uri, String[] projection, String limit) {
188        final String searchClause;
189        final String selection;
190        if (uri.getPathSegments().size() <= 1) {
191            searchClause = null;
192            selection = RECENTLY_CONTACTED;
193        } else {
194            searchClause = uri.getLastPathSegment();
195            selection = null;
196        }
197
198        if (!TextUtils.isEmpty(searchClause) && TextUtils.isDigitsOnly(searchClause)
199                && mContactsProvider.isPhone()) {
200            return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause);
201        } else {
202            return buildCursorForSearchSuggestionsBasedOnFilter(
203                    db, projection, selection, searchClause, limit);
204        }
205    }
206
207    /**
208     * Returns a search suggestions cursor for the contact bearing the provided lookup key.  If the
209     * lookup key cannot be found in the database, the contact name is decoded from the lookup key
210     * and used to re-identify the contact.  If the contact still cannot be found, an empty cursor
211     * is returned.
212     *
213     * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned
214     * silently.  This would occur with old-style shortcuts that were created using the contact id
215     * instead of the lookup key.
216     */
217    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection,
218            String lookupKey, String filter) {
219        long contactId;
220        try {
221            contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey);
222        } catch (IllegalArgumentException e) {
223            contactId = -1L;
224        }
225        return buildCursorForSearchSuggestionsBasedOnFilter(
226                db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null);
227    }
228
229    private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
230        MatrixCursor cursor = new MatrixCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS);
231        Resources r = mContactsProvider.getContext().getResources();
232        String s;
233        int i;
234
235        ArrayList<Object> dialNumber = new ArrayList<Object>();
236        dialNumber.add(0);  // _id
237        s = r.getString(com.android.internal.R.string.dial_number_using, searchClause);
238        i = s.indexOf('\n');
239        if (i < 0) {
240            dialNumber.add(s);
241            dialNumber.add("");
242        } else {
243            dialNumber.add(s.substring(0, i));
244            dialNumber.add(s.substring(i + 1));
245        }
246        dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact));
247        dialNumber.add("tel:" + searchClause);
248        dialNumber.add(ContactsContract.Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
249        dialNumber.add(null);
250        cursor.addRow(dialNumber);
251
252        ArrayList<Object> createContact = new ArrayList<Object>();
253        createContact.add(1);  // _id
254        s = r.getString(com.android.internal.R.string.create_contact_using, searchClause);
255        i = s.indexOf('\n');
256        if (i < 0) {
257            createContact.add(s);
258            createContact.add("");
259        } else {
260            createContact.add(s.substring(0, i));
261            createContact.add(s.substring(i + 1));
262        }
263        createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact));
264        createContact.add("tel:" + searchClause);
265        createContact.add(ContactsContract.Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
266        createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
267        cursor.addRow(createContact);
268
269        return cursor;
270    }
271
272    private Cursor buildCursorForSearchSuggestionsBasedOnFilter(SQLiteDatabase db,
273            String[] projection, String selection, String filter, String limit) {
274        MatrixCursor cursor = new MatrixCursor(
275                projection != null ? projection : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS);
276        StringBuilder sb = new StringBuilder();
277        final boolean haveFilter = !TextUtils.isEmpty(filter);
278        sb.append("SELECT "
279                        + Contacts._ID + ", "
280                        + Contacts.LOOKUP_KEY + ", "
281                        + Contacts.PHOTO_THUMBNAIL_URI + ", "
282                        + Contacts.DISPLAY_NAME + ", "
283                        + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE);
284        if (haveFilter) {
285            sb.append(", " + SearchSnippetColumns.SNIPPET);
286        }
287        sb.append(" FROM ");
288        sb.append(getDatabaseHelper().getContactView(false));
289        sb.append(" AS contacts");
290        if (haveFilter) {
291            mContactsProvider.appendSearchIndexJoin(sb, filter, true,
292                    String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH),
293                    SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS);
294        }
295        if (selection != null) {
296            sb.append(" WHERE ").append(selection);
297        }
298        sb.append(" ORDER BY " + SORT_ORDER);
299        if (limit != null) {
300            sb.append(" LIMIT " + limit);
301        }
302        Cursor c = new SnippetizingCursorWrapper(
303                db.rawQuery(sb.toString(), null),
304                haveFilter ? filter : "",
305                String.valueOf(SNIPPET_START_MATCH),
306                String.valueOf(SNIPPET_END_MATCH),
307                SNIPPET_ELLIPSIS,
308                SNIPPET_MAX_TOKENS);
309        SearchSuggestion suggestion = new SearchSuggestion();
310        suggestion.filter = filter;
311        try {
312            while (c.moveToNext()) {
313                suggestion.contactId = c.getLong(0);
314                suggestion.lookupKey = c.getString(1);
315                suggestion.photoUri = c.getString(2);
316                suggestion.text1 = c.getString(3);
317                suggestion.presence = c.isNull(4) ? -1 : c.getInt(4);
318                if (haveFilter) {
319                    suggestion.text2 = shortenSnippet(c.getString(5));
320                }
321                cursor.addRow(suggestion.asList(projection));
322            }
323        } finally {
324            c.close();
325        }
326        return cursor;
327    }
328
329    private ContactsDatabaseHelper getDatabaseHelper() {
330        return (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper();
331    }
332
333    private String shortenSnippet(final String snippet) {
334        if (snippet == null) {
335            return null;
336        }
337
338        int from = 0;
339        int to = snippet.length();
340        int start = snippet.indexOf(SNIPPET_START_MATCH);
341        if (start == -1) {
342            return null;
343        }
344
345        int firstNl = snippet.lastIndexOf('\n', start);
346        if (firstNl != -1) {
347            from = firstNl + 1;
348        }
349        int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
350        if (end != -1) {
351            int lastNl = snippet.indexOf('\n', end);
352            if (lastNl != -1) {
353                to = lastNl;
354            }
355        }
356
357        StringBuilder sb = new StringBuilder();
358        for (int i = from; i < to; i++) {
359            char c = snippet.charAt(i);
360            if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
361                sb.append(c);
362            }
363        }
364        return sb.toString();
365    }
366}
367