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