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 
17 package com.android.providers.contacts;
18 
19 import android.app.SearchManager;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.MatrixCursor;
23 import android.database.sqlite.SQLiteDatabase;
24 import android.net.Uri;
25 import android.os.CancellationSignal;
26 import android.provider.ContactsContract.CommonDataKinds.Email;
27 import android.provider.ContactsContract.CommonDataKinds.Organization;
28 import android.provider.ContactsContract.CommonDataKinds.Phone;
29 import android.provider.ContactsContract.Contacts;
30 import android.provider.ContactsContract.Data;
31 import android.provider.ContactsContract.SearchSnippets;
32 import android.provider.ContactsContract.StatusUpdates;
33 import android.telephony.TelephonyManager;
34 import android.text.TextUtils;
35 
36 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
37 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
38 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
39 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
40 
41 import java.util.ArrayList;
42 
43 /**
44  * Support for global search integration for Contacts.
45  */
46 public 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"})
asList(String[] projection)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 
addColumnValue(ArrayList<Object> list, String column)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 
buildUri()145         private String buildUri() {
146             return Contacts.getLookupUri(contactId, lookupKey).toString();
147         }
148 
reset()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")
GlobalSearchSupport(ContactsProvider2 contactsProvider)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 
handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String[] projection, String limit, CancellationSignal cancellationSignal)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      */
handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection, String lookupKey, String filter, CancellationSignal cancellationSignal)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 
addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db, String[] projection, String selection, String filter, String limit, CancellationSignal cancellationSignal)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         sb.append(" WHERE " + Contacts.LOOKUP_KEY + " IS NOT NULL");
248         if (selection != null) {
249             sb.append(" AND ").append(selection);
250         }
251         if (limit != null) {
252             sb.append(" LIMIT " + limit);
253         }
254         Cursor c = db.rawQuery(sb.toString(), null, cancellationSignal);
255         SearchSuggestion suggestion = new SearchSuggestion();
256         suggestion.filter = filter;
257         try {
258             while (c.moveToNext()) {
259                 suggestion.contactId = c.getLong(0);
260                 suggestion.lookupKey = c.getString(1);
261                 suggestion.photoUri = c.getString(2);
262                 suggestion.text1 = c.getString(3);
263                 suggestion.presence = c.isNull(4) ? -1 : c.getInt(4);
264                 suggestion.lastAccessTime = c.getString(5);
265                 if (haveFilter) {
266                     suggestion.text2 = shortenSnippet(c.getString(6));
267                 }
268                 cursor.addRow(suggestion.asList(projection));
269                 suggestion.reset();
270             }
271         } finally {
272             c.close();
273         }
274         return cursor;
275     }
276 
shortenSnippet(final String snippet)277     private String shortenSnippet(final String snippet) {
278         if (snippet == null) {
279             return null;
280         }
281 
282         int from = 0;
283         int to = snippet.length();
284         int start = snippet.indexOf(SNIPPET_START_MATCH);
285         if (start == -1) {
286             return null;
287         }
288 
289         int firstNl = snippet.lastIndexOf('\n', start);
290         if (firstNl != -1) {
291             from = firstNl + 1;
292         }
293         int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
294         if (end != -1) {
295             int lastNl = snippet.indexOf('\n', end);
296             if (lastNl != -1) {
297                 to = lastNl;
298             }
299         }
300 
301         StringBuilder sb = new StringBuilder();
302         for (int i = from; i < to; i++) {
303             char c = snippet.charAt(i);
304             if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
305                 sb.append(c);
306             }
307         }
308         return sb.toString();
309     }
310 }
311