1 /* 2 * Copyright (C) 2009 Google Inc. 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.mms; 18 19 import java.util.ArrayList; 20 21 import android.app.SearchManager; 22 import android.content.ContentResolver; 23 import android.content.ContentValues; 24 import android.content.Intent; 25 import android.database.CharArrayBuffer; 26 import android.database.ContentObserver; 27 import android.database.CrossProcessCursor; 28 import android.database.Cursor; 29 import android.database.CursorWindow; 30 import android.database.DataSetObserver; 31 import android.database.sqlite.SQLiteException; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.text.TextUtils; 35 36 /** 37 * Suggestions provider for mms. Queries the "words" table to provide possible word suggestions. 38 */ 39 public class SuggestionsProvider extends android.content.ContentProvider { 40 41 final static String AUTHORITY = "com.android.mms.SuggestionsProvider"; 42 // final static int MODE = DATABASE_MODE_QUERIES + DATABASE_MODE_2LINES; 43 44 public SuggestionsProvider() { 45 super(); 46 } 47 48 @Override 49 public int delete(Uri uri, String selection, String[] selectionArgs) { 50 return 0; 51 } 52 53 @Override 54 public String getType(Uri uri) { 55 return null; 56 } 57 58 @Override 59 public Uri insert(Uri uri, ContentValues values) { 60 return null; 61 } 62 63 @Override 64 public boolean onCreate() { 65 return true; 66 } 67 68 @Override 69 public Cursor query(Uri uri, String[] projection, String selection, 70 String[] selectionArgs, String sortOrder) { 71 Uri u = Uri.parse(String.format( 72 "content://mms-sms/searchSuggest?pattern=%s", 73 selectionArgs[0])); 74 Cursor c = getContext().getContentResolver().query( 75 u, 76 null, 77 null, 78 null, 79 null); 80 81 return new SuggestionsCursor(c, selectionArgs[0]); 82 } 83 84 @Override 85 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 86 return 0; 87 } 88 89 private class SuggestionsCursor implements CrossProcessCursor { 90 Cursor mDatabaseCursor; 91 int mColumnCount; 92 int mCurrentRow; 93 ArrayList<Row> mRows = new ArrayList<Row>(); 94 String mQuery; 95 96 public SuggestionsCursor(Cursor cursor, String query) { 97 mDatabaseCursor = cursor; 98 mQuery = query; 99 100 mColumnCount = cursor.getColumnCount(); 101 try { 102 computeRows(); 103 } catch (SQLiteException ex) { 104 // This can happen if the user enters -n (anything starting with -). 105 // sqlite3/fts3 can't handle it. Google for "logic error or missing database fts3" 106 // for commentary on it. 107 mRows.clear(); // assume no results 108 } 109 } 110 111 public int getCount() { 112 return mRows.size(); 113 } 114 115 private class Row { 116 private String mSnippet; 117 private int mRowNumber; 118 119 public Row(int row, String snippet) { 120 mSnippet = snippet.trim(); 121 mRowNumber = row; 122 } 123 public String getSnippet() { 124 return mSnippet; 125 } 126 } 127 128 /* 129 * Compute rows for rows in the cursor. The cursor can contain duplicates which 130 * are filtered out in the while loop. Using DISTINCT on the result of the 131 * FTS3 snippet function does not work so we do it here in the code. 132 */ 133 private void computeRows() { 134 int snippetColumn = mDatabaseCursor.getColumnIndex("snippet"); 135 136 int count = mDatabaseCursor.getCount(); 137 String previousSnippet = null; 138 139 for (int i = 0; i < count; i++) { 140 mDatabaseCursor.moveToPosition(i); 141 String snippet = mDatabaseCursor.getString(snippetColumn); 142 if (!TextUtils.equals(previousSnippet, snippet)) { 143 mRows.add(new Row(i, snippet)); 144 previousSnippet = snippet; 145 } 146 } 147 } 148 149 private int [] computeOffsets(String offsetsString) { 150 String [] vals = offsetsString.split(" "); 151 152 int [] retvals = new int[vals.length]; 153 for (int i = retvals.length-1; i >= 0; i--) { 154 retvals[i] = Integer.parseInt(vals[i]); 155 } 156 return retvals; 157 } 158 159 public void fillWindow(int position, CursorWindow window) { 160 int count = getCount(); 161 if (position < 0 || position > count + 1) { 162 return; 163 } 164 window.acquireReference(); 165 try { 166 int oldpos = getPosition(); 167 int pos = position; 168 window.clear(); 169 window.setStartPosition(position); 170 int columnNum = getColumnCount(); 171 window.setNumColumns(columnNum); 172 while (moveToPosition(pos) && window.allocRow()) { 173 for (int i = 0; i < columnNum; i++) { 174 String field = getString(i); 175 if (field != null) { 176 if (!window.putString(field, pos, i)) { 177 window.freeLastRow(); 178 break; 179 } 180 } else { 181 if (!window.putNull(pos, i)) { 182 window.freeLastRow(); 183 break; 184 } 185 } 186 } 187 ++pos; 188 } 189 moveToPosition(oldpos); 190 } catch (IllegalStateException e){ 191 // simply ignore it 192 } finally { 193 window.releaseReference(); 194 } 195 } 196 197 public CursorWindow getWindow() { 198 return null; 199 } 200 201 public boolean onMove(int oldPosition, int newPosition) { 202 return ((CrossProcessCursor)mDatabaseCursor).onMove(oldPosition, newPosition); 203 } 204 205 /* 206 * These "virtual columns" are columns which don't exist in the underlying 207 * database cursor but are exported by this cursor. For example, we compute 208 * a "word" by taking the substring of the full row text in the words table 209 * using the provided offsets. 210 */ 211 private String [] mVirtualColumns = new String [] { 212 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 213 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 214 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, 215 SearchManager.SUGGEST_COLUMN_TEXT_1, 216 }; 217 218 // Cursor column offsets for the above virtual columns. 219 // These columns exist after the natural columns in the 220 // database cursor. So, for example, the column called 221 // SUGGEST_COLUMN_TEXT_1 comes 3 after mDatabaseCursor.getColumnCount(). 222 private final int INTENT_DATA_COLUMN = 0; 223 private final int INTENT_ACTION_COLUMN = 1; 224 private final int INTENT_EXTRA_DATA_COLUMN = 2; 225 private final int INTENT_TEXT_COLUMN = 3; 226 227 228 public int getColumnCount() { 229 return mColumnCount + mVirtualColumns.length; 230 } 231 232 public int getColumnIndex(String columnName) { 233 for (int i = 0; i < mVirtualColumns.length; i++) { 234 if (mVirtualColumns[i].equals(columnName)) { 235 return mColumnCount + i; 236 } 237 } 238 return mDatabaseCursor.getColumnIndex(columnName); 239 } 240 241 public String [] getColumnNames() { 242 String [] x = mDatabaseCursor.getColumnNames(); 243 String [] y = new String [x.length + mVirtualColumns.length]; 244 245 for (int i = 0; i < x.length; i++) { 246 y[i] = x[i]; 247 } 248 249 for (int i = 0; i < mVirtualColumns.length; i++) { 250 y[x.length + i] = mVirtualColumns[i]; 251 } 252 253 return y; 254 } 255 256 public boolean moveToPosition(int position) { 257 if (position >= 0 && position < mRows.size()) { 258 mCurrentRow = position; 259 mDatabaseCursor.moveToPosition(mRows.get(position).mRowNumber); 260 return true; 261 } else { 262 return false; 263 } 264 } 265 266 public boolean move(int offset) { 267 return moveToPosition(mCurrentRow + offset); 268 } 269 270 public boolean moveToFirst() { 271 return moveToPosition(0); 272 } 273 274 public boolean moveToLast() { 275 return moveToPosition(mRows.size() - 1); 276 } 277 278 public boolean moveToNext() { 279 return moveToPosition(mCurrentRow + 1); 280 } 281 282 public boolean moveToPrevious() { 283 return moveToPosition(mCurrentRow - 1); 284 } 285 286 public String getString(int column) { 287 // if we're returning one of the columns in the underlying database column 288 // then do so here 289 if (column < mColumnCount) { 290 return mDatabaseCursor.getString(column); 291 } 292 293 // otherwise we're returning one of the synthetic columns. 294 // the constants like INTENT_DATA_COLUMN are offsets relative to 295 // mColumnCount. 296 Row row = mRows.get(mCurrentRow); 297 switch (column - mColumnCount) { 298 case INTENT_DATA_COLUMN: 299 Uri.Builder b = Uri.parse("content://mms-sms/search").buildUpon(); 300 b = b.appendQueryParameter("pattern", row.getSnippet()); 301 Uri u = b.build(); 302 return u.toString(); 303 case INTENT_ACTION_COLUMN: 304 return Intent.ACTION_SEARCH; 305 case INTENT_EXTRA_DATA_COLUMN: 306 return row.getSnippet(); 307 case INTENT_TEXT_COLUMN: 308 return row.getSnippet(); 309 default: 310 return null; 311 } 312 } 313 314 public void close() { 315 mDatabaseCursor.close(); 316 } 317 318 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { 319 mDatabaseCursor.copyStringToBuffer(columnIndex, buffer); 320 } 321 322 public void deactivate() { 323 mDatabaseCursor.deactivate(); 324 } 325 326 public byte[] getBlob(int columnIndex) { 327 return null; 328 } 329 330 public int getColumnIndexOrThrow(String columnName) 331 throws IllegalArgumentException { 332 return 0; 333 } 334 335 public String getColumnName(int columnIndex) { 336 return null; 337 } 338 339 public double getDouble(int columnIndex) { 340 return 0; 341 } 342 343 public Bundle getExtras() { 344 return Bundle.EMPTY; 345 } 346 347 public float getFloat(int columnIndex) { 348 return 0; 349 } 350 351 public int getInt(int columnIndex) { 352 return 0; 353 } 354 355 public long getLong(int columnIndex) { 356 return 0; 357 } 358 359 public int getPosition() { 360 return mCurrentRow; 361 } 362 363 public short getShort(int columnIndex) { 364 return 0; 365 } 366 367 public boolean getWantsAllOnMoveCalls() { 368 return false; 369 } 370 371 public boolean isAfterLast() { 372 return mCurrentRow >= mRows.size(); 373 } 374 375 public boolean isBeforeFirst() { 376 return mCurrentRow < 0; 377 } 378 379 public boolean isClosed() { 380 return mDatabaseCursor.isClosed(); 381 } 382 383 public boolean isFirst() { 384 return mCurrentRow == 0; 385 } 386 387 public boolean isLast() { 388 return mCurrentRow == mRows.size() - 1; 389 } 390 391 public int getType(int columnIndex) { 392 throw new UnsupportedOperationException(); // TODO revisit 393 } 394 395 public boolean isNull(int columnIndex) { 396 return false; // TODO revisit 397 } 398 399 public void registerContentObserver(ContentObserver observer) { 400 mDatabaseCursor.registerContentObserver(observer); 401 } 402 403 public void registerDataSetObserver(DataSetObserver observer) { 404 mDatabaseCursor.registerDataSetObserver(observer); 405 } 406 407 public boolean requery() { 408 return false; 409 } 410 411 public Bundle respond(Bundle extras) { 412 return mDatabaseCursor.respond(extras); 413 } 414 415 public void setNotificationUri(ContentResolver cr, Uri uri) { 416 mDatabaseCursor.setNotificationUri(cr, uri); 417 } 418 419 public Uri getNotificationUri() { 420 return mDatabaseCursor.getNotificationUri(); 421 } 422 423 public void unregisterContentObserver(ContentObserver observer) { 424 mDatabaseCursor.unregisterContentObserver(observer); 425 } 426 427 public void unregisterDataSetObserver(DataSetObserver observer) { 428 mDatabaseCursor.unregisterDataSetObserver(observer); 429 } 430 } 431 } 432