1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.providers;
19
20import android.app.SearchManager;
21import android.content.ContentProvider;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.UriMatcher;
26import android.database.Cursor;
27import android.database.sqlite.SQLiteDatabase;
28import android.database.sqlite.SQLiteOpenHelper;
29import android.net.Uri;
30import android.text.TextUtils;
31
32import com.android.mail.R;
33
34import java.util.ArrayList;
35
36public class SearchRecentSuggestionsProvider extends ContentProvider {
37    /*
38     * String used to delimit different parts of a query.
39     */
40    public static final String QUERY_TOKEN_SEPARATOR = " ";
41
42    // client-provided configuration values
43    private String mAuthority;
44    private int mMode;
45
46    // general database configuration and tables
47    private SQLiteOpenHelper mOpenHelper;
48    private static final String sDatabaseName = "suggestions.db";
49    private static final String sSuggestions = "suggestions";
50    private static final String ORDER_BY = "date DESC";
51    private static final String NULL_COLUMN = "query";
52
53    // Table of database versions.  Don't forget to update!
54    // NOTE:  These version values are shifted left 8 bits (x 256) in order to create space for
55    // a small set of mode bitflags in the version int.
56    //
57    // 1      original implementation with queries, and 1 or 2 display columns
58    // 1->2   added UNIQUE constraint to display1 column
59    private static final int DATABASE_VERSION = 2 * 256;
60
61    /**
62     * This mode bit configures the database to record recent queries.  <i>required</i>
63     *
64     * @see #setupSuggestions(String, int)
65     */
66    public static final int DATABASE_MODE_QUERIES = 1;
67
68    // Uri and query support
69    private static final int URI_MATCH_SUGGEST = 1;
70
71    private Uri mSuggestionsUri;
72    private UriMatcher mUriMatcher;
73
74    private String mSuggestSuggestionClause;
75    private String[] mSuggestionProjection;
76
77    /**
78     * Builds the database.  This version has extra support for using the version field
79     * as a mode flags field, and configures the database columns depending on the mode bits
80     * (features) requested by the extending class.
81     *
82     * @hide
83     */
84    private static class DatabaseHelper extends SQLiteOpenHelper {
85        public DatabaseHelper(Context context, int newVersion) {
86            super(context, sDatabaseName, null, newVersion);
87        }
88
89        @Override
90        public void onCreate(SQLiteDatabase db) {
91            StringBuilder builder = new StringBuilder();
92            builder.append("CREATE TABLE suggestions (" +
93                    "_id INTEGER PRIMARY KEY" +
94                    ",display1 TEXT UNIQUE ON CONFLICT REPLACE" +
95                    ",query TEXT" +
96                    ",date LONG" +
97                    ");");
98            db.execSQL(builder.toString());
99        }
100
101        @Override
102        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
103            db.execSQL("DROP TABLE IF EXISTS suggestions");
104            onCreate(db);
105        }
106    }
107
108    /**
109     * In order to use this class, you must extend it, and call this setup function from your
110     * constructor.  In your application or activities, you must provide the same values when
111     * you create the {@link android.provider.SearchRecentSuggestions} helper.
112     *
113     * @param authority This must match the authority that you've declared in your manifest.
114     * @param mode You can use mode flags here to determine certain functional aspects of your
115     * database.  Note, this value should not change from run to run, because when it does change,
116     * your suggestions database may be wiped.
117     *
118     * @see #DATABASE_MODE_QUERIES
119     */
120    protected void setupSuggestions(String authority, int mode) {
121        if (TextUtils.isEmpty(authority) ||
122                ((mode & DATABASE_MODE_QUERIES) == 0)) {
123            throw new IllegalArgumentException();
124        }
125
126        // saved values
127        mAuthority = new String(authority);
128        mMode = mode;
129
130        // derived values
131        mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions");
132        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
133        mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST);
134
135        // The URI of the icon that we will include on every suggestion here.
136        final String historicalIcon = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
137                + getContext().getPackageName() + "/" + R.drawable.ic_history_holo_light;
138
139        mSuggestSuggestionClause = "display1 LIKE ?";
140        mSuggestionProjection = new String [] {
141                "_id",
142                "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1,
143                "query AS " + SearchManager.SUGGEST_COLUMN_QUERY,
144                "'" + historicalIcon + "' AS " + SearchManager.SUGGEST_COLUMN_ICON_1
145        };
146    }
147
148    /**
149     * This method is provided for use by the ContentResolver.  Do not override, or directly
150     * call from your own code.
151     */
152    @Override
153    public int delete(Uri uri, String selection, String[] selectionArgs) {
154        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
155
156        final int length = uri.getPathSegments().size();
157        if (length != 1) {
158            throw new IllegalArgumentException("Unknown Uri");
159        }
160
161        final String base = uri.getPathSegments().get(0);
162        int count = 0;
163        if (base.equals(sSuggestions)) {
164            count = db.delete(sSuggestions, selection, selectionArgs);
165        } else {
166            throw new IllegalArgumentException("Unknown Uri");
167        }
168        getContext().getContentResolver().notifyChange(uri, null);
169        return count;
170    }
171
172    /**
173     * This method is provided for use by the ContentResolver.  Do not override, or directly
174     * call from your own code.
175     */
176    @Override
177    public String getType(Uri uri) {
178        if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) {
179            return SearchManager.SUGGEST_MIME_TYPE;
180        }
181        int length = uri.getPathSegments().size();
182        if (length >= 1) {
183            String base = uri.getPathSegments().get(0);
184            if (base.equals(sSuggestions)) {
185                if (length == 1) {
186                    return "vnd.android.cursor.dir/suggestion";
187                } else if (length == 2) {
188                    return "vnd.android.cursor.item/suggestion";
189                }
190            }
191        }
192        throw new IllegalArgumentException("Unknown Uri");
193    }
194
195    /**
196     * This method is provided for use by the ContentResolver.  Do not override, or directly
197     * call from your own code.
198     */
199    @Override
200    public Uri insert(Uri uri, ContentValues values) {
201        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
202
203        int length = uri.getPathSegments().size();
204        if (length < 1) {
205            throw new IllegalArgumentException("Unknown Uri");
206        }
207        // Note:  This table has on-conflict-replace semantics, so insert() may actually replace()
208        long rowID = -1;
209        String base = uri.getPathSegments().get(0);
210        Uri newUri = null;
211        if (base.equals(sSuggestions)) {
212            if (length == 1) {
213                rowID = db.insert(sSuggestions, NULL_COLUMN, values);
214                if (rowID > 0) {
215                    newUri = Uri.withAppendedPath(mSuggestionsUri, String.valueOf(rowID));
216                }
217            }
218        }
219        if (rowID < 0) {
220            throw new IllegalArgumentException("Unknown Uri");
221        }
222        getContext().getContentResolver().notifyChange(newUri, null);
223        return newUri;
224    }
225
226    /**
227     * This method is provided for use by the ContentResolver.  Do not override, or directly
228     * call from your own code.
229     */
230    @Override
231    public boolean onCreate() {
232        if (mAuthority == null || mMode == 0) {
233            throw new IllegalArgumentException("Provider not configured");
234        }
235        int mWorkingDbVersion = DATABASE_VERSION + mMode;
236        mOpenHelper = new DatabaseHelper(getContext(), mWorkingDbVersion);
237
238        return true;
239    }
240
241    private ArrayList<String> mFullQueryTerms;
242
243    /**
244     *  Copy the projection, and change the query field alone.
245     * @param selectionArgs
246     * @return projection
247     */
248    private String[] createProjection(String[] selectionArgs) {
249        String[] newProjection = new String[mSuggestionProjection.length];
250        String queryAs;
251        int fullSize = (mFullQueryTerms != null) ? mFullQueryTerms.size() : 0;
252        if (fullSize > 0) {
253            String realQuery = "'";
254            for (int i = 0; i < fullSize; i++) {
255                realQuery+= mFullQueryTerms.get(i);
256                if (i < fullSize -1) {
257                    realQuery += QUERY_TOKEN_SEPARATOR;
258                }
259            }
260            queryAs = realQuery + " ' || query AS " + SearchManager.SUGGEST_COLUMN_QUERY;
261        } else {
262            queryAs = "query AS " + SearchManager.SUGGEST_COLUMN_QUERY;
263        }
264        for (int i = 0; i < mSuggestionProjection.length; i++) {
265            newProjection[i] = mSuggestionProjection[i];
266        }
267        // Assumes that newProjection[length-2] is the query field.
268        newProjection[mSuggestionProjection.length - 2] = queryAs;
269        return newProjection;
270    }
271
272    /**
273     * Set the other query terms to be included in the user's query.
274     * These are in addition to what is being looked up for suggestions.
275     * @param terms
276     */
277    public void setFullQueryTerms(ArrayList<String> terms) {
278        mFullQueryTerms = terms;
279    }
280
281    /**
282     * This method is provided for use by the ContentResolver. Do not override,
283     * or directly call from your own code.
284     */
285    // TODO: Confirm no injection attacks here, or rewrite.
286    @Override
287    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
288            String sortOrder) {
289        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
290
291        // special case for actual suggestions (from search manager)
292        String suggestSelection;
293        String[] myArgs;
294        if (TextUtils.isEmpty(selectionArgs[0])) {
295            suggestSelection = null;
296            myArgs = null;
297        } else {
298            String like = "%" + selectionArgs[0] + "%";
299            myArgs = new String[] { like };
300            suggestSelection = mSuggestSuggestionClause;
301        }
302        // Suggestions are always performed with the default sort order
303        // Add this to the query:
304        // "select 'real_query' as SearchManager.SUGGEST_COLUMN_QUERY.
305        // rest of query
306        // real query will then show up in the suggestion
307        Cursor c = db.query(sSuggestions, createProjection(selectionArgs), suggestSelection, myArgs,
308                null, null, ORDER_BY, null);
309        c.setNotificationUri(getContext().getContentResolver(), uri);
310        return c;
311    }
312
313    /**
314     * This method is provided for use by the ContentResolver.  Do not override, or directly
315     * call from your own code.
316     */
317    @Override
318    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
319        throw new UnsupportedOperationException("Not implemented");
320    }
321}