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
17package com.android.mms;
18
19import java.util.ArrayList;
20import java.util.Map;
21
22import android.app.SearchManager;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Intent;
26import android.database.CharArrayBuffer;
27import android.database.ContentObserver;
28import android.database.CrossProcessCursor;
29import android.database.Cursor;
30import android.database.CursorWindow;
31import android.database.DataSetObserver;
32import android.database.sqlite.SQLiteException;
33import android.net.Uri;
34import android.os.Bundle;
35import android.text.TextUtils;
36
37/**
38 * Suggestions provider for mms.  Queries the "words" table to provide possible word suggestions.
39 */
40public 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
45    public SuggestionsProvider() {
46        super();
47    }
48
49    @Override
50    public int delete(Uri uri, String selection, String[] selectionArgs) {
51        return 0;
52    }
53
54    @Override
55    public String getType(Uri uri) {
56        return null;
57    }
58
59    @Override
60    public Uri insert(Uri uri, ContentValues values) {
61        return null;
62    }
63
64    @Override
65    public boolean onCreate() {
66        return true;
67    }
68
69    @Override
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
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
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
112        public int getCount() {
113            return mRows.size();
114        }
115
116        private class Row {
117            private String mSnippet;
118            private int mRowNumber;
119
120            public Row(int row, String snippet) {
121                mSnippet = snippet.trim();
122                mRowNumber = row;
123            }
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         */
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
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
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
198        public CursorWindow getWindow() {
199            return null;
200        }
201
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
229        public int getColumnCount() {
230            return mColumnCount + mVirtualColumns.length;
231        }
232
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
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
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
267        public boolean move(int offset) {
268            return moveToPosition(mCurrentRow + offset);
269        }
270
271        public boolean moveToFirst() {
272            return moveToPosition(0);
273        }
274
275        public boolean moveToLast() {
276            return moveToPosition(mRows.size() - 1);
277        }
278
279        public boolean moveToNext() {
280            return moveToPosition(mCurrentRow + 1);
281        }
282
283        public boolean moveToPrevious() {
284            return moveToPosition(mCurrentRow - 1);
285        }
286
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
315        public void close() {
316            mDatabaseCursor.close();
317        }
318
319        public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
320            mDatabaseCursor.copyStringToBuffer(columnIndex, buffer);
321        }
322
323        public void deactivate() {
324            mDatabaseCursor.deactivate();
325        }
326
327        public byte[] getBlob(int columnIndex) {
328            return null;
329        }
330
331        public int getColumnIndexOrThrow(String columnName)
332                throws IllegalArgumentException {
333            return 0;
334        }
335
336        public String getColumnName(int columnIndex) {
337            return null;
338        }
339
340        public double getDouble(int columnIndex) {
341            return 0;
342        }
343
344        public Bundle getExtras() {
345            return Bundle.EMPTY;
346        }
347
348        public float getFloat(int columnIndex) {
349            return 0;
350        }
351
352        public int getInt(int columnIndex) {
353            return 0;
354        }
355
356        public long getLong(int columnIndex) {
357            return 0;
358        }
359
360        public int getPosition() {
361            return mCurrentRow;
362        }
363
364        public short getShort(int columnIndex) {
365            return 0;
366        }
367
368        public boolean getWantsAllOnMoveCalls() {
369            return false;
370        }
371
372        public boolean isAfterLast() {
373            return mCurrentRow >= mRows.size();
374        }
375
376        public boolean isBeforeFirst() {
377            return mCurrentRow < 0;
378        }
379
380        public boolean isClosed() {
381            return mDatabaseCursor.isClosed();
382        }
383
384        public boolean isFirst() {
385            return mCurrentRow == 0;
386        }
387
388        public boolean isLast() {
389            return mCurrentRow == mRows.size() - 1;
390        }
391
392        public int getType(int columnIndex) {
393            throw new UnsupportedOperationException();  // TODO revisit
394        }
395
396        public boolean isNull(int columnIndex) {
397            return false;  // TODO revisit
398        }
399
400        public void registerContentObserver(ContentObserver observer) {
401            mDatabaseCursor.registerContentObserver(observer);
402        }
403
404        public void registerDataSetObserver(DataSetObserver observer) {
405            mDatabaseCursor.registerDataSetObserver(observer);
406        }
407
408        public boolean requery() {
409            return false;
410        }
411
412        public Bundle respond(Bundle extras) {
413            return mDatabaseCursor.respond(extras);
414        }
415
416        public void setNotificationUri(ContentResolver cr, Uri uri) {
417            mDatabaseCursor.setNotificationUri(cr, uri);
418        }
419
420        public void unregisterContentObserver(ContentObserver observer) {
421            mDatabaseCursor.unregisterContentObserver(observer);
422        }
423
424        public void unregisterDataSetObserver(DataSetObserver observer) {
425            mDatabaseCursor.unregisterDataSetObserver(observer);
426        }
427    }
428}
429