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 android.app.SearchManager;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.MatrixCursor;
23import android.database.sqlite.SQLiteDatabase;
24import android.net.Uri;
25import android.provider.ContactsContract.CommonDataKinds.Email;
26import android.provider.ContactsContract.CommonDataKinds.Organization;
27import android.provider.ContactsContract.CommonDataKinds.Phone;
28import android.provider.ContactsContract.Contacts;
29import android.provider.ContactsContract.Data;
30import android.provider.ContactsContract.SearchSnippetColumns;
31import android.provider.ContactsContract.StatusUpdates;
32import android.telephony.TelephonyManager;
33import android.text.TextUtils;
34
35import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
36import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
37import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
38import com.android.providers.contacts.ContactsDatabaseHelper.Views;
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_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_ICON_2,
53            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
54            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
55            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
56            SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
57            SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT,
58    };
59
60    private static final char SNIPPET_START_MATCH = '\u0001';
61    private static final char SNIPPET_END_MATCH = '\u0001';
62    private static final String SNIPPET_ELLIPSIS = "\u2026";
63    private static final int SNIPPET_MAX_TOKENS = 5;
64
65    private static final String PRESENCE_SQL =
66        "(SELECT " + StatusUpdates.PRESENCE +
67        " FROM " + Tables.AGGREGATED_PRESENCE +
68        " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
69
70    private static class SearchSuggestion {
71        long contactId;
72        String photoUri;
73        String lookupKey;
74        int presence = -1;
75        String text1;
76        String text2;
77        String icon1;
78        String icon2;
79        String intentData;
80        String intentAction;
81        String filter;
82        String lastAccessTime;
83
84        @SuppressWarnings({"unchecked"})
85        public ArrayList<?> asList(String[] projection) {
86            if (icon1 == null) {
87                if (photoUri != null) {
88                    icon1 = photoUri.toString();
89                } else {
90                    icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
91                }
92            }
93
94            if (presence != -1) {
95                icon2 = String.valueOf(StatusUpdates.getPresenceIconResourceId(presence));
96            }
97
98            ArrayList<Object> list = new ArrayList<Object>();
99            if (projection == null) {
100                list.add(contactId); // _id
101                list.add(text1); // text1
102                list.add(text2); // text2
103                list.add(icon1); // icon1
104                list.add(icon2); // icon2
105                list.add(intentData == null ? buildUri() : intentData); // intent data
106                list.add(intentAction); // intentAction
107                list.add(lookupKey); // shortcut id
108                list.add(filter); // extra data
109                list.add(lastAccessTime); // last access hint
110            } else {
111                for (int i = 0; i < projection.length; i++) {
112                    addColumnValue(list, projection[i]);
113                }
114            }
115            return list;
116        }
117
118        private void addColumnValue(ArrayList<Object> list, String column) {
119            if ("_id".equals(column)) {
120                list.add(contactId);
121            } else if (SearchManager.SUGGEST_COLUMN_TEXT_1.equals(column)) {
122                list.add(text1);
123            } else if (SearchManager.SUGGEST_COLUMN_TEXT_2.equals(column)) {
124                list.add(text2);
125            } else if (SearchManager.SUGGEST_COLUMN_ICON_1.equals(column)) {
126                list.add(icon1);
127            } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) {
128                list.add(icon2);
129            } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA.equals(column)) {
130                list.add(intentData == null ? buildUri() : intentData);
131            } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) {
132                list.add(lookupKey);
133            } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) {
134                list.add(lookupKey);
135            } else if (SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA.equals(column)) {
136                list.add(filter);
137            } else if (SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT.equals(column)) {
138                list.add(lastAccessTime);
139            } else {
140                throw new IllegalArgumentException("Invalid column name: " + column);
141            }
142        }
143
144        private String buildUri() {
145            return Contacts.getLookupUri(contactId, lookupKey).toString();
146        }
147
148        public void reset() {
149            contactId = 0;
150            photoUri = null;
151            lookupKey = null;
152            presence = -1;
153            text1 = null;
154            text2 = null;
155            icon1 = null;
156            icon2 = null;
157            intentData = null;
158            intentAction = null;
159            filter = null;
160            lastAccessTime = null;
161        }
162    }
163
164    private final ContactsProvider2 mContactsProvider;
165
166    @SuppressWarnings("all")
167    public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
168        mContactsProvider = contactsProvider;
169
170        TelephonyManager telman = (TelephonyManager)
171                mContactsProvider.getContext().getSystemService(Context.TELEPHONY_SERVICE);
172
173        // To ensure the data column position. This is dead code if properly configured.
174        if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
175                || Email.DATA != Data.DATA1) {
176            throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
177                    + " data is not in DATA1 column");
178        }
179    }
180
181    public Cursor handleSearchSuggestionsQuery(
182            SQLiteDatabase db, Uri uri, String[] projection, String limit) {
183        final MatrixCursor cursor = new MatrixCursor(
184                projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
185
186        if (uri.getPathSegments().size() <= 1) {
187            // no search term, return empty
188        } else {
189            String selection = null;
190            String searchClause = uri.getLastPathSegment();
191            addSearchSuggestionsBasedOnFilter(
192                    cursor, db, projection, selection, searchClause, limit);
193        }
194
195        return cursor;
196    }
197
198    /**
199     * Returns a search suggestions cursor for the contact bearing the provided lookup key.  If the
200     * lookup key cannot be found in the database, the contact name is decoded from the lookup key
201     * and used to re-identify the contact.  If the contact still cannot be found, an empty cursor
202     * is returned.
203     *
204     * <p>Note that if {@code lookupKey} is not a valid lookup key, an empty cursor is returned
205     * silently.  This would occur with old-style shortcuts that were created using the contact id
206     * instead of the lookup key.
207     */
208    public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection,
209            String lookupKey, String filter) {
210        long contactId;
211        try {
212            contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey);
213        } catch (IllegalArgumentException e) {
214            contactId = -1L;
215        }
216        MatrixCursor cursor = new MatrixCursor(
217                projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
218        return addSearchSuggestionsBasedOnFilter(cursor,
219                db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null);
220    }
221
222    private Cursor addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db,
223            String[] projection, String selection, String filter, String limit) {
224        StringBuilder sb = new StringBuilder();
225        final boolean haveFilter = !TextUtils.isEmpty(filter);
226        sb.append("SELECT "
227                        + Contacts._ID + ", "
228                        + Contacts.LOOKUP_KEY + ", "
229                        + Contacts.PHOTO_THUMBNAIL_URI + ", "
230                        + Contacts.DISPLAY_NAME + ", "
231                        + PRESENCE_SQL + " AS " + Contacts.CONTACT_PRESENCE + ", "
232                        + Contacts.LAST_TIME_CONTACTED);
233        if (haveFilter) {
234            sb.append(", " + SearchSnippetColumns.SNIPPET);
235        }
236        sb.append(" FROM ");
237        sb.append(Views.CONTACTS);
238        sb.append(" AS contacts");
239        if (haveFilter) {
240            mContactsProvider.appendSearchIndexJoin(sb, filter, true,
241                    String.valueOf(SNIPPET_START_MATCH), String.valueOf(SNIPPET_END_MATCH),
242                    SNIPPET_ELLIPSIS, SNIPPET_MAX_TOKENS, false);
243        }
244        if (selection != null) {
245            sb.append(" WHERE ").append(selection);
246        }
247        if (limit != null) {
248            sb.append(" LIMIT " + limit);
249        }
250        Cursor c = db.rawQuery(sb.toString(), null);
251        SearchSuggestion suggestion = new SearchSuggestion();
252        suggestion.filter = filter;
253        try {
254            while (c.moveToNext()) {
255                suggestion.contactId = c.getLong(0);
256                suggestion.lookupKey = c.getString(1);
257                suggestion.photoUri = c.getString(2);
258                suggestion.text1 = c.getString(3);
259                suggestion.presence = c.isNull(4) ? -1 : c.getInt(4);
260                suggestion.lastAccessTime = c.getString(5);
261                if (haveFilter) {
262                    suggestion.text2 = shortenSnippet(c.getString(6));
263                }
264                cursor.addRow(suggestion.asList(projection));
265                suggestion.reset();
266            }
267        } finally {
268            c.close();
269        }
270        return cursor;
271    }
272
273    private String shortenSnippet(final String snippet) {
274        if (snippet == null) {
275            return null;
276        }
277
278        int from = 0;
279        int to = snippet.length();
280        int start = snippet.indexOf(SNIPPET_START_MATCH);
281        if (start == -1) {
282            return null;
283        }
284
285        int firstNl = snippet.lastIndexOf('\n', start);
286        if (firstNl != -1) {
287            from = firstNl + 1;
288        }
289        int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
290        if (end != -1) {
291            int lastNl = snippet.indexOf('\n', end);
292            if (lastNl != -1) {
293                to = lastNl;
294            }
295        }
296
297        StringBuilder sb = new StringBuilder();
298        for (int i = from; i < to; i++) {
299            char c = snippet.charAt(i);
300            if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
301                sb.append(c);
302            }
303        }
304        return sb.toString();
305    }
306}
307