• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.provider.ContactsContract.CommonDataKinds.Email;
26 import android.provider.ContactsContract.CommonDataKinds.Organization;
27 import android.provider.ContactsContract.CommonDataKinds.Phone;
28 import android.provider.ContactsContract.Contacts;
29 import android.provider.ContactsContract.Data;
30 import android.provider.ContactsContract.SearchSnippetColumns;
31 import android.provider.ContactsContract.StatusUpdates;
32 import android.telephony.TelephonyManager;
33 import android.text.TextUtils;
34 
35 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
36 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
37 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
38 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
39 
40 import java.util.ArrayList;
41 
42 /**
43  * Support for global search integration for Contacts.
44  */
45 public 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"})
asList(String[] projection)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 
addColumnValue(ArrayList<Object> list, String column)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 
buildUri()144         private String buildUri() {
145             return Contacts.getLookupUri(contactId, lookupKey).toString();
146         }
147 
reset()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")
GlobalSearchSupport(ContactsProvider2 contactsProvider)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 
handleSearchSuggestionsQuery( SQLiteDatabase db, Uri uri, String[] projection, String limit)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      */
handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection, String lookupKey, String filter)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 
addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db, String[] projection, String selection, String filter, String limit)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 
shortenSnippet(final String snippet)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