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