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 }