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