Home | History | Annotate | Download | only in contacts
      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"})
     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         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 
    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