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.ContentResolver;
22import android.content.ContentValues;
23import android.content.Context;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.support.annotation.Nullable;
28
29import com.android.mail.R;
30
31import java.util.ArrayList;
32
33public class SearchRecentSuggestionsProvider {
34    /*
35     * String used to delimit different parts of a query.
36     */
37    public static final String QUERY_TOKEN_SEPARATOR = " ";
38
39    // general database configuration and tables
40    private SQLiteOpenHelper mOpenHelper;
41    private static final String DATABASE_NAME = "suggestions.db";
42    private static final String SUGGESTIONS_TABLE = "suggestions";
43
44    private static final String QUERY =
45            " SELECT _id" +
46            "   , display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 +
47            "   , ? || query AS " + SearchManager.SUGGEST_COLUMN_QUERY +
48            "   , ? AS " + SearchManager.SUGGEST_COLUMN_ICON_1 +
49            " FROM " + SUGGESTIONS_TABLE +
50            " WHERE display1 LIKE ?" +
51            " ORDER BY date DESC";
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    // 2->3   <redacted> being dumb and accidentally upgraded, this should be ignored.
60    private static final int DATABASE_VERSION = 3 * 256;
61
62    private static final int DATABASE_VERSION_2 = 2 * 256;
63    private static final int DATABASE_VERSION_3 = 3 * 256;
64
65    private String mHistoricalIcon;
66
67    protected final Context mContext;
68    private ArrayList<String> mFullQueryTerms;
69
70    private final Object mDbLock = new Object();
71    private boolean mClosed;
72
73    public SearchRecentSuggestionsProvider(Context context) {
74        mContext = context;
75        mOpenHelper = new DatabaseHelper(mContext, DATABASE_VERSION);
76
77        // The URI of the icon that we will include on every suggestion here.
78        mHistoricalIcon = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
79                + mContext.getPackageName() + "/" + R.drawable.ic_history_24dp;
80    }
81
82    public void cleanup() {
83        synchronized (mDbLock) {
84            mOpenHelper.close();
85            mClosed = true;
86        }
87    }
88
89    /**
90     * Builds the database.  This version has extra support for using the version field
91     * as a mode flags field, and configures the database columns depending on the mode bits
92     * (features) requested by the extending class.
93     *
94     * @hide
95     */
96    private static class DatabaseHelper extends SQLiteOpenHelper {
97        public DatabaseHelper(Context context, int newVersion) {
98            super(context, DATABASE_NAME, null, newVersion);
99        }
100
101        @Override
102        public void onCreate(SQLiteDatabase db) {
103            final String create = "CREATE TABLE suggestions (" +
104                    "_id INTEGER PRIMARY KEY" +
105                    ",display1 TEXT UNIQUE ON CONFLICT REPLACE" +
106                    ",query TEXT" +
107                    ",date LONG" +
108                    ");";
109            db.execSQL(create);
110        }
111
112        @Override
113        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
114            // When checking the old version clear the last 8 bits
115            oldVersion = oldVersion & ~0xff;
116            newVersion = newVersion & ~0xff;
117            if (oldVersion == DATABASE_VERSION_2 && newVersion == DATABASE_VERSION_3) {
118                // Oops, didn't mean to upgrade this database. Ignore this upgrade.
119                return;
120            }
121            db.execSQL("DROP TABLE IF EXISTS suggestions");
122            onCreate(db);
123        }
124    }
125
126    /**
127     * Set the other query terms to be included in the user's query.
128     * These are in addition to what is being looked up for suggestions.
129     * @param terms
130     */
131    public void setFullQueryTerms(ArrayList<String> terms) {
132        mFullQueryTerms = terms;
133    }
134
135    private @Nullable SQLiteDatabase getDatabase(boolean readOnly) {
136        synchronized (mDbLock) {
137            if (!mClosed) {
138                return readOnly ? mOpenHelper.getReadableDatabase() :
139                        mOpenHelper.getWritableDatabase();
140            }
141        }
142        return null;
143    }
144
145    public Cursor query(String query) {
146        final SQLiteDatabase db = getDatabase(true /* readOnly */);
147        if (db != null) {
148            final StringBuilder builder = new StringBuilder();
149            if (mFullQueryTerms != null) {
150                for (String token : mFullQueryTerms) {
151                    builder.append(token).append(QUERY_TOKEN_SEPARATOR);
152                }
153            }
154
155            final String[] args = new String[] {
156                    builder.toString(), mHistoricalIcon, "%" + query + "%" };
157
158            try {
159                // db could have been closed due to cleanup, simply don't do anything.
160                return db.rawQuery(QUERY, args);
161            } catch (IllegalStateException e) {}
162        }
163        return null;
164    }
165
166    /**
167     * We are going to keep track of recent suggestions ourselves and not depend on the framework.
168     * Note that this writes to disk. DO NOT CALL FROM MAIN THREAD.
169     */
170    public void saveRecentQuery(String query) {
171        final SQLiteDatabase db = getDatabase(false /* readOnly */);
172        if (db != null) {
173            ContentValues values = new ContentValues(3);
174            values.put("display1", query);
175            values.put("query", query);
176            values.put("date", System.currentTimeMillis());
177            // Note:  This table has on-conflict-replace semantics, so insert may actually replace
178            try {
179                // db could have been closed due to cleanup, simply don't do anything.
180                db.insert(SUGGESTIONS_TABLE, null, values);
181            } catch (IllegalStateException e) {}
182        }
183    }
184
185    public void clearHistory() {
186        final SQLiteDatabase db = getDatabase(false /* readOnly */);
187        if (db != null) {
188            try {
189                // db could have been closed due to cleanup, simply don't do anything.
190                db.delete(SUGGESTIONS_TABLE, null, null);
191            } catch (IllegalStateException e) {}
192        }
193    }
194}