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;
20
21import android.app.SearchManager;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.Intent;
25import android.database.CharArrayBuffer;
26import android.database.ContentObserver;
27import android.database.CrossProcessCursor;
28import android.database.Cursor;
29import android.database.CursorWindow;
30import android.database.DataSetObserver;
31import android.database.sqlite.SQLiteException;
32import android.net.Uri;
33import android.os.Bundle;
34import android.text.TextUtils;
35
36/**
37 * Suggestions provider for mms.  Queries the "words" table to provide possible word suggestions.
38 */
39public 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