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 18 package com.android.mail.providers; 19 20 import android.app.SearchManager; 21 import android.content.ContentResolver; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.database.sqlite.SQLiteOpenHelper; 27 import android.support.annotation.Nullable; 28 29 import com.android.mail.R; 30 31 import java.util.ArrayList; 32 33 public 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 }