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 
SuggestionsProvider()44     public SuggestionsProvider() {
45         super();
46     }
47 
48     @Override
delete(Uri uri, String selection, String[] selectionArgs)49     public int delete(Uri uri, String selection, String[] selectionArgs) {
50         return 0;
51     }
52 
53     @Override
getType(Uri uri)54     public String getType(Uri uri) {
55         return null;
56     }
57 
58     @Override
insert(Uri uri, ContentValues values)59     public Uri insert(Uri uri, ContentValues values) {
60         return null;
61     }
62 
63     @Override
onCreate()64     public boolean onCreate() {
65         return true;
66     }
67 
68     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)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 
SuggestionsCursor(Cursor cursor, String query)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 
getCount()111         public int getCount() {
112             return mRows.size();
113         }
114 
115         private class Row {
116             private String mSnippet;
117             private int mRowNumber;
118 
Row(int row, String snippet)119             public Row(int row, String snippet) {
120                 mSnippet = snippet.trim();
121                 mRowNumber = row;
122             }
getSnippet()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          */
computeRows()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 
computeOffsets(String offsetsString)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 
fillWindow(int position, CursorWindow window)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 
getWindow()197         public CursorWindow getWindow() {
198             return null;
199         }
200 
onMove(int oldPosition, int newPosition)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 
getColumnCount()228         public int getColumnCount() {
229             return mColumnCount + mVirtualColumns.length;
230         }
231 
getColumnIndex(String columnName)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 
getColumnNames()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 
moveToPosition(int position)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 
move(int offset)266         public boolean move(int offset) {
267             return moveToPosition(mCurrentRow + offset);
268         }
269 
moveToFirst()270         public boolean moveToFirst() {
271             return moveToPosition(0);
272         }
273 
moveToLast()274         public boolean moveToLast() {
275             return moveToPosition(mRows.size() - 1);
276         }
277 
moveToNext()278         public boolean moveToNext() {
279             return moveToPosition(mCurrentRow + 1);
280         }
281 
moveToPrevious()282         public boolean moveToPrevious() {
283             return moveToPosition(mCurrentRow - 1);
284         }
285 
getString(int column)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 
close()314         public void close() {
315             mDatabaseCursor.close();
316         }
317 
copyStringToBuffer(int columnIndex, CharArrayBuffer buffer)318         public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
319             mDatabaseCursor.copyStringToBuffer(columnIndex, buffer);
320         }
321 
deactivate()322         public void deactivate() {
323             mDatabaseCursor.deactivate();
324         }
325 
getBlob(int columnIndex)326         public byte[] getBlob(int columnIndex) {
327             return null;
328         }
329 
getColumnIndexOrThrow(String columnName)330         public int getColumnIndexOrThrow(String columnName)
331                 throws IllegalArgumentException {
332             return 0;
333         }
334 
getColumnName(int columnIndex)335         public String getColumnName(int columnIndex) {
336             return null;
337         }
338 
getDouble(int columnIndex)339         public double getDouble(int columnIndex) {
340             return 0;
341         }
342 
getExtras()343         public Bundle getExtras() {
344             return Bundle.EMPTY;
345         }
346 
getFloat(int columnIndex)347         public float getFloat(int columnIndex) {
348             return 0;
349         }
350 
getInt(int columnIndex)351         public int getInt(int columnIndex) {
352             return 0;
353         }
354 
getLong(int columnIndex)355         public long getLong(int columnIndex) {
356             return 0;
357         }
358 
getPosition()359         public int getPosition() {
360             return mCurrentRow;
361         }
362 
getShort(int columnIndex)363         public short getShort(int columnIndex) {
364             return 0;
365         }
366 
getWantsAllOnMoveCalls()367         public boolean getWantsAllOnMoveCalls() {
368             return false;
369         }
370 
isAfterLast()371         public boolean isAfterLast() {
372             return mCurrentRow >= mRows.size();
373         }
374 
isBeforeFirst()375         public boolean isBeforeFirst() {
376             return mCurrentRow < 0;
377         }
378 
isClosed()379         public boolean isClosed() {
380             return mDatabaseCursor.isClosed();
381         }
382 
isFirst()383         public boolean isFirst() {
384             return mCurrentRow == 0;
385         }
386 
isLast()387         public boolean isLast() {
388             return mCurrentRow == mRows.size() - 1;
389         }
390 
getType(int columnIndex)391         public int getType(int columnIndex) {
392             throw new UnsupportedOperationException();  // TODO revisit
393         }
394 
isNull(int columnIndex)395         public boolean isNull(int columnIndex) {
396             return false;  // TODO revisit
397         }
398 
registerContentObserver(ContentObserver observer)399         public void registerContentObserver(ContentObserver observer) {
400             mDatabaseCursor.registerContentObserver(observer);
401         }
402 
registerDataSetObserver(DataSetObserver observer)403         public void registerDataSetObserver(DataSetObserver observer) {
404             mDatabaseCursor.registerDataSetObserver(observer);
405         }
406 
requery()407         public boolean requery() {
408             return false;
409         }
410 
respond(Bundle extras)411         public Bundle respond(Bundle extras) {
412             return mDatabaseCursor.respond(extras);
413         }
414 
setNotificationUri(ContentResolver cr, Uri uri)415         public void setNotificationUri(ContentResolver cr, Uri uri) {
416             mDatabaseCursor.setNotificationUri(cr, uri);
417         }
418 
getNotificationUri()419         public Uri getNotificationUri() {
420             return mDatabaseCursor.getNotificationUri();
421         }
422 
unregisterContentObserver(ContentObserver observer)423         public void unregisterContentObserver(ContentObserver observer) {
424             mDatabaseCursor.unregisterContentObserver(observer);
425         }
426 
unregisterDataSetObserver(DataSetObserver observer)427         public void unregisterDataSetObserver(DataSetObserver observer) {
428             mDatabaseCursor.unregisterDataSetObserver(observer);
429         }
430     }
431 }
432