Home | History | Annotate | Download | only in providers
      1 /*
      2  * Copyright (C) 2012 Google Inc.
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mail.providers;
     19 
     20 import android.app.SearchManager;
     21 import android.content.ContentProvider;
     22 import android.content.ContentResolver;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.UriMatcher;
     26 import android.database.Cursor;
     27 import android.database.sqlite.SQLiteDatabase;
     28 import android.database.sqlite.SQLiteOpenHelper;
     29 import android.net.Uri;
     30 import android.text.TextUtils;
     31 
     32 import com.android.mail.R;
     33 
     34 import java.util.ArrayList;
     35 
     36 public class SearchRecentSuggestionsProvider extends ContentProvider {
     37     /*
     38      * String used to delimit different parts of a query.
     39      */
     40     public static final String QUERY_TOKEN_SEPARATOR = " ";
     41 
     42     // client-provided configuration values
     43     private String mAuthority;
     44     private int mMode;
     45 
     46     // general database configuration and tables
     47     private SQLiteOpenHelper mOpenHelper;
     48     private static final String sDatabaseName = "suggestions.db";
     49     private static final String sSuggestions = "suggestions";
     50     private static final String ORDER_BY = "date DESC";
     51     private static final String NULL_COLUMN = "query";
     52 
     53     // Table of database versions.  Don't forget to update!
     54     // NOTE:  These version values are shifted left 8 bits (x 256) in order to create space for
     55     // a small set of mode bitflags in the version int.
     56     //
     57     // 1      original implementation with queries, and 1 or 2 display columns
     58     // 1->2   added UNIQUE constraint to display1 column
     59     private static final int DATABASE_VERSION = 2 * 256;
     60 
     61     /**
     62      * This mode bit configures the database to record recent queries.  <i>required</i>
     63      *
     64      * @see #setupSuggestions(String, int)
     65      */
     66     public static final int DATABASE_MODE_QUERIES = 1;
     67 
     68     // Uri and query support
     69     private static final int URI_MATCH_SUGGEST = 1;
     70 
     71     private Uri mSuggestionsUri;
     72     private UriMatcher mUriMatcher;
     73 
     74     private String mSuggestSuggestionClause;
     75     private String[] mSuggestionProjection;
     76 
     77     /**
     78      * Builds the database.  This version has extra support for using the version field
     79      * as a mode flags field, and configures the database columns depending on the mode bits
     80      * (features) requested by the extending class.
     81      *
     82      * @hide
     83      */
     84     private static class DatabaseHelper extends SQLiteOpenHelper {
     85         public DatabaseHelper(Context context, int newVersion) {
     86             super(context, sDatabaseName, null, newVersion);
     87         }
     88 
     89         @Override
     90         public void onCreate(SQLiteDatabase db) {
     91             StringBuilder builder = new StringBuilder();
     92             builder.append("CREATE TABLE suggestions (" +
     93                     "_id INTEGER PRIMARY KEY" +
     94                     ",display1 TEXT UNIQUE ON CONFLICT REPLACE" +
     95                     ",query TEXT" +
     96                     ",date LONG" +
     97                     ");");
     98             db.execSQL(builder.toString());
     99         }
    100 
    101         @Override
    102         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    103             db.execSQL("DROP TABLE IF EXISTS suggestions");
    104             onCreate(db);
    105         }
    106     }
    107 
    108     /**
    109      * In order to use this class, you must extend it, and call this setup function from your
    110      * constructor.  In your application or activities, you must provide the same values when
    111      * you create the {@link android.provider.SearchRecentSuggestions} helper.
    112      *
    113      * @param authority This must match the authority that you've declared in your manifest.
    114      * @param mode You can use mode flags here to determine certain functional aspects of your
    115      * database.  Note, this value should not change from run to run, because when it does change,
    116      * your suggestions database may be wiped.
    117      *
    118      * @see #DATABASE_MODE_QUERIES
    119      */
    120     protected void setupSuggestions(String authority, int mode) {
    121         if (TextUtils.isEmpty(authority) ||
    122                 ((mode & DATABASE_MODE_QUERIES) == 0)) {
    123             throw new IllegalArgumentException();
    124         }
    125 
    126         // saved values
    127         mAuthority = new String(authority);
    128         mMode = mode;
    129 
    130         // derived values
    131         mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
    132         mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    133         mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST);
    134 
    135         // The URI of the icon that we will include on every suggestion here.
    136         final String historicalIcon = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
    137                 + getContext().getPackageName() + "/" + R.drawable.ic_history_holo_light;
    138 
    139         mSuggestSuggestionClause = "display1 LIKE ?";
    140         mSuggestionProjection = new String [] {
    141                 "_id",
    142                 "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
    143                 "query AS " + SearchManager.SUGGEST_COLUMN_QUERY,
    144                 "'" + historicalIcon + "' AS " + SearchManager.SUGGEST_COLUMN_ICON_1
    145         };
    146     }
    147 
    148     /**
    149      * This method is provided for use by the ContentResolver.  Do not override, or directly
    150      * call from your own code.
    151      */
    152     @Override
    153     public int delete(Uri uri, String selection, String[] selectionArgs) {
    154         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    155 
    156         final int length = uri.getPathSegments().size();
    157         if (length != 1) {
    158             throw new IllegalArgumentException("Unknown Uri");
    159         }
    160 
    161         final String base = uri.getPathSegments().get(0);
    162         int count = 0;
    163         if (base.equals(sSuggestions)) {
    164             count = db.delete(sSuggestions, selection, selectionArgs);
    165         } else {
    166             throw new IllegalArgumentException("Unknown Uri");
    167         }
    168         getContext().getContentResolver().notifyChange(uri, null);
    169         return count;
    170     }
    171 
    172     /**
    173      * This method is provided for use by the ContentResolver.  Do not override, or directly
    174      * call from your own code.
    175      */
    176     @Override
    177     public String getType(Uri uri) {
    178         if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) {
    179             return SearchManager.SUGGEST_MIME_TYPE;
    180         }
    181         int length = uri.getPathSegments().size();
    182         if (length >= 1) {
    183             String base = uri.getPathSegments().get(0);
    184             if (base.equals(sSuggestions)) {
    185                 if (length == 1) {
    186                     return "vnd.android.cursor.dir/suggestion";
    187                 } else if (length == 2) {
    188                     return "vnd.android.cursor.item/suggestion";
    189                 }
    190             }
    191         }
    192         throw new IllegalArgumentException("Unknown Uri");
    193     }
    194 
    195     /**
    196      * This method is provided for use by the ContentResolver.  Do not override, or directly
    197      * call from your own code.
    198      */
    199     @Override
    200     public Uri insert(Uri uri, ContentValues values) {
    201         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    202 
    203         int length = uri.getPathSegments().size();
    204         if (length < 1) {
    205             throw new IllegalArgumentException("Unknown Uri");
    206         }
    207         // Note:  This table has on-conflict-replace semantics, so insert() may actually replace()
    208         long rowID = -1;
    209         String base = uri.getPathSegments().get(0);
    210         Uri newUri = null;
    211         if (base.equals(sSuggestions)) {
    212             if (length == 1) {
    213                 rowID = db.insert(sSuggestions, NULL_COLUMN, values);
    214                 if (rowID > 0) {
    215                     newUri = Uri.withAppendedPath(mSuggestionsUri, String.valueOf(rowID));
    216                 }
    217             }
    218         }
    219         if (rowID < 0) {
    220             throw new IllegalArgumentException("Unknown Uri");
    221         }
    222         getContext().getContentResolver().notifyChange(newUri, null);
    223         return newUri;
    224     }
    225 
    226     /**
    227      * This method is provided for use by the ContentResolver.  Do not override, or directly
    228      * call from your own code.
    229      */
    230     @Override
    231     public boolean onCreate() {
    232         if (mAuthority == null || mMode == 0) {
    233             throw new IllegalArgumentException("Provider not configured");
    234         }
    235         int mWorkingDbVersion = DATABASE_VERSION + mMode;
    236         mOpenHelper = new DatabaseHelper(getContext(), mWorkingDbVersion);
    237 
    238         return true;
    239     }
    240 
    241     private ArrayList<String> mFullQueryTerms;
    242 
    243     /**
    244      *  Copy the projection, and change the query field alone.
    245      * @param selectionArgs
    246      * @return projection
    247      */
    248     private String[] createProjection(String[] selectionArgs) {
    249         String[] newProjection = new String[mSuggestionProjection.length];
    250         String queryAs;
    251         int fullSize = (mFullQueryTerms != null) ? mFullQueryTerms.size() : 0;
    252         if (fullSize > 0) {
    253             String realQuery = "'";
    254             for (int i = 0; i < fullSize; i++) {
    255                 realQuery+= mFullQueryTerms.get(i);
    256                 if (i < fullSize -1) {
    257                     realQuery += QUERY_TOKEN_SEPARATOR;
    258                 }
    259             }
    260             queryAs = realQuery + " ' || query AS " + SearchManager.SUGGEST_COLUMN_QUERY;
    261         } else {
    262             queryAs = "query AS " + SearchManager.SUGGEST_COLUMN_QUERY;
    263         }
    264         for (int i = 0; i < mSuggestionProjection.length; i++) {
    265             newProjection[i] = mSuggestionProjection[i];
    266         }
    267         // Assumes that newProjection[length-2] is the query field.
    268         newProjection[mSuggestionProjection.length - 2] = queryAs;
    269         return newProjection;
    270     }
    271 
    272     /**
    273      * Set the other query terms to be included in the user's query.
    274      * These are in addition to what is being looked up for suggestions.
    275      * @param terms
    276      */
    277     public void setFullQueryTerms(ArrayList<String> terms) {
    278         mFullQueryTerms = terms;
    279     }
    280 
    281     /**
    282      * This method is provided for use by the ContentResolver. Do not override,
    283      * or directly call from your own code.
    284      */
    285     // TODO: Confirm no injection attacks here, or rewrite.
    286     @Override
    287     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    288             String sortOrder) {
    289         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
    290 
    291         // special case for actual suggestions (from search manager)
    292         String suggestSelection;
    293         String[] myArgs;
    294         if (TextUtils.isEmpty(selectionArgs[0])) {
    295             suggestSelection = null;
    296             myArgs = null;
    297         } else {
    298             String like = "%" + selectionArgs[0] + "%";
    299             myArgs = new String[] { like };
    300             suggestSelection = mSuggestSuggestionClause;
    301         }
    302         // Suggestions are always performed with the default sort order
    303         // Add this to the query:
    304         // "select 'real_query' as SearchManager.SUGGEST_COLUMN_QUERY.
    305         // rest of query
    306         // real query will then show up in the suggestion
    307         Cursor c = db.query(sSuggestions, createProjection(selectionArgs), suggestSelection, myArgs,
    308                 null, null, ORDER_BY, null);
    309         c.setNotificationUri(getContext().getContentResolver(), uri);
    310         return c;
    311     }
    312 
    313     /**
    314      * This method is provided for use by the ContentResolver.  Do not override, or directly
    315      * call from your own code.
    316      */
    317     @Override
    318     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    319         throw new UnsupportedOperationException("Not implemented");
    320     }
    321 }