1 /* 2 * Copyright (C) 2014 The Android Open Source Project 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 17 package com.android.settings.search; 18 19 import android.content.Context; 20 import android.content.pm.ResolveInfo; 21 import android.database.Cursor; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.database.sqlite.SQLiteOpenHelper; 24 import android.os.Build; 25 import android.support.annotation.VisibleForTesting; 26 import android.text.TextUtils; 27 import android.util.Log; 28 29 import java.util.List; 30 31 public class IndexDatabaseHelper extends SQLiteOpenHelper { 32 33 private static final String TAG = "IndexDatabaseHelper"; 34 35 private static final String DATABASE_NAME = "search_index.db"; 36 private static final int DATABASE_VERSION = 117; 37 38 private static final String INDEX = "index"; 39 40 private static final String PREF_KEY_INDEXED_PROVIDERS = "indexed_providers"; 41 42 public interface Tables { 43 String TABLE_PREFS_INDEX = "prefs_index"; 44 String TABLE_SITE_MAP = "site_map"; 45 String TABLE_META_INDEX = "meta_index"; 46 String TABLE_SAVED_QUERIES = "saved_queries"; 47 } 48 49 public interface IndexColumns { 50 String DOCID = "docid"; 51 String LOCALE = "locale"; 52 String DATA_RANK = "data_rank"; 53 String DATA_TITLE = "data_title"; 54 String DATA_TITLE_NORMALIZED = "data_title_normalized"; 55 String DATA_SUMMARY_ON = "data_summary_on"; 56 String DATA_SUMMARY_ON_NORMALIZED = "data_summary_on_normalized"; 57 String DATA_SUMMARY_OFF = "data_summary_off"; 58 String DATA_SUMMARY_OFF_NORMALIZED = "data_summary_off_normalized"; 59 String DATA_ENTRIES = "data_entries"; 60 String DATA_KEYWORDS = "data_keywords"; 61 String CLASS_NAME = "class_name"; 62 String SCREEN_TITLE = "screen_title"; 63 String INTENT_ACTION = "intent_action"; 64 String INTENT_TARGET_PACKAGE = "intent_target_package"; 65 String INTENT_TARGET_CLASS = "intent_target_class"; 66 String ICON = "icon"; 67 String ENABLED = "enabled"; 68 String DATA_KEY_REF = "data_key_reference"; 69 String USER_ID = "user_id"; 70 String PAYLOAD_TYPE = "payload_type"; 71 String PAYLOAD = "payload"; 72 } 73 74 public interface MetaColumns { 75 String BUILD = "build"; 76 } 77 78 public interface SavedQueriesColumns { 79 String QUERY = "query"; 80 String TIME_STAMP = "timestamp"; 81 } 82 83 public interface SiteMapColumns { 84 String DOCID = "docid"; 85 String PARENT_CLASS = "parent_class"; 86 String CHILD_CLASS = "child_class"; 87 String PARENT_TITLE = "parent_title"; 88 String CHILD_TITLE = "child_title"; 89 } 90 91 private static final String CREATE_INDEX_TABLE = 92 "CREATE VIRTUAL TABLE " + Tables.TABLE_PREFS_INDEX + " USING fts4" + 93 "(" + 94 IndexColumns.LOCALE + 95 ", " + 96 IndexColumns.DATA_RANK + 97 ", " + 98 IndexColumns.DATA_TITLE + 99 ", " + 100 IndexColumns.DATA_TITLE_NORMALIZED + 101 ", " + 102 IndexColumns.DATA_SUMMARY_ON + 103 ", " + 104 IndexColumns.DATA_SUMMARY_ON_NORMALIZED + 105 ", " + 106 IndexColumns.DATA_SUMMARY_OFF + 107 ", " + 108 IndexColumns.DATA_SUMMARY_OFF_NORMALIZED + 109 ", " + 110 IndexColumns.DATA_ENTRIES + 111 ", " + 112 IndexColumns.DATA_KEYWORDS + 113 ", " + 114 IndexColumns.SCREEN_TITLE + 115 ", " + 116 IndexColumns.CLASS_NAME + 117 ", " + 118 IndexColumns.ICON + 119 ", " + 120 IndexColumns.INTENT_ACTION + 121 ", " + 122 IndexColumns.INTENT_TARGET_PACKAGE + 123 ", " + 124 IndexColumns.INTENT_TARGET_CLASS + 125 ", " + 126 IndexColumns.ENABLED + 127 ", " + 128 IndexColumns.DATA_KEY_REF + 129 ", " + 130 IndexColumns.USER_ID + 131 ", " + 132 IndexColumns.PAYLOAD_TYPE + 133 ", " + 134 IndexColumns.PAYLOAD + 135 ");"; 136 137 private static final String CREATE_META_TABLE = 138 "CREATE TABLE " + Tables.TABLE_META_INDEX + 139 "(" + 140 MetaColumns.BUILD + " VARCHAR(32) NOT NULL" + 141 ")"; 142 143 private static final String CREATE_SAVED_QUERIES_TABLE = 144 "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES + 145 "(" + 146 SavedQueriesColumns.QUERY + " VARCHAR(64) NOT NULL" + 147 ", " + 148 SavedQueriesColumns.TIME_STAMP + " INTEGER" + 149 ")"; 150 151 private static final String CREATE_SITE_MAP_TABLE = 152 "CREATE VIRTUAL TABLE " + Tables.TABLE_SITE_MAP + " USING fts4" + 153 "(" + 154 SiteMapColumns.PARENT_CLASS + 155 ", " + 156 SiteMapColumns.CHILD_CLASS + 157 ", " + 158 SiteMapColumns.PARENT_TITLE + 159 ", " + 160 SiteMapColumns.CHILD_TITLE + 161 ")"; 162 private static final String INSERT_BUILD_VERSION = 163 "INSERT INTO " + Tables.TABLE_META_INDEX + 164 " VALUES ('" + Build.VERSION.INCREMENTAL + "');"; 165 166 private static final String SELECT_BUILD_VERSION = 167 "SELECT " + MetaColumns.BUILD + " FROM " + Tables.TABLE_META_INDEX + " LIMIT 1;"; 168 169 private static IndexDatabaseHelper sSingleton; 170 171 private final Context mContext; 172 getInstance(Context context)173 public static synchronized IndexDatabaseHelper getInstance(Context context) { 174 if (sSingleton == null) { 175 sSingleton = new IndexDatabaseHelper(context); 176 } 177 return sSingleton; 178 } 179 IndexDatabaseHelper(Context context)180 public IndexDatabaseHelper(Context context) { 181 super(context, DATABASE_NAME, null, DATABASE_VERSION); 182 mContext = context; 183 } 184 185 @Override onCreate(SQLiteDatabase db)186 public void onCreate(SQLiteDatabase db) { 187 bootstrapDB(db); 188 } 189 bootstrapDB(SQLiteDatabase db)190 private void bootstrapDB(SQLiteDatabase db) { 191 db.execSQL(CREATE_INDEX_TABLE); 192 db.execSQL(CREATE_META_TABLE); 193 db.execSQL(CREATE_SAVED_QUERIES_TABLE); 194 db.execSQL(CREATE_SITE_MAP_TABLE); 195 db.execSQL(INSERT_BUILD_VERSION); 196 Log.i(TAG, "Bootstrapped database"); 197 } 198 199 @Override onOpen(SQLiteDatabase db)200 public void onOpen(SQLiteDatabase db) { 201 super.onOpen(db); 202 203 Log.i(TAG, "Using schema version: " + db.getVersion()); 204 205 if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) { 206 Log.w(TAG, "Index needs to be rebuilt as build-version is not the same"); 207 // We need to drop the tables and recreate them 208 reconstruct(db); 209 } else { 210 Log.i(TAG, "Index is fine"); 211 } 212 } 213 214 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)215 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 216 if (oldVersion < DATABASE_VERSION) { 217 Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + 218 "Index needs to be rebuilt for schema version '" + newVersion + "'."); 219 // We need to drop the tables and recreate them 220 reconstruct(db); 221 } 222 } 223 224 @Override onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)225 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 226 Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + 227 "Index needs to be rebuilt for schema version '" + newVersion + "'."); 228 // We need to drop the tables and recreate them 229 reconstruct(db); 230 } 231 reconstruct(SQLiteDatabase db)232 public void reconstruct(SQLiteDatabase db) { 233 dropTables(db); 234 bootstrapDB(db); 235 } 236 getBuildVersion(SQLiteDatabase db)237 private String getBuildVersion(SQLiteDatabase db) { 238 String version = null; 239 Cursor cursor = null; 240 try { 241 cursor = db.rawQuery(SELECT_BUILD_VERSION, null); 242 if (cursor.moveToFirst()) { 243 version = cursor.getString(0); 244 } 245 } catch (Exception e) { 246 Log.e(TAG, "Cannot get build version from Index metadata"); 247 } finally { 248 if (cursor != null) { 249 cursor.close(); 250 } 251 } 252 return version; 253 } 254 255 /** 256 * Perform a full index on an OTA or when the locale has changed 257 * 258 * @param locale is the default for the device 259 * @param fingerprint id for the current build. 260 * @return true when the locale or build has changed since last index. 261 */ 262 @VisibleForTesting isFullIndex(Context context, String locale, String fingerprint, String providerVersionedNames)263 static boolean isFullIndex(Context context, String locale, String fingerprint, 264 String providerVersionedNames) { 265 final boolean isLocaleIndexed = IndexDatabaseHelper.isLocaleAlreadyIndexed(context, locale); 266 final boolean isBuildIndexed = IndexDatabaseHelper.isBuildIndexed(context, fingerprint); 267 final boolean areProvidersIndexed = IndexDatabaseHelper 268 .areProvidersIndexed(context, providerVersionedNames); 269 270 return !(isLocaleIndexed && isBuildIndexed && areProvidersIndexed); 271 } 272 273 @VisibleForTesting buildProviderVersionedNames(List<ResolveInfo> providers)274 static String buildProviderVersionedNames(List<ResolveInfo> providers) { 275 StringBuilder sb = new StringBuilder(); 276 for (ResolveInfo info : providers) { 277 sb.append(info.providerInfo.packageName) 278 .append(':') 279 .append(info.providerInfo.applicationInfo.versionCode) 280 .append(','); 281 } 282 return sb.toString(); 283 } 284 clearCachedIndexed(Context context)285 static void clearCachedIndexed(Context context) { 286 context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).edit().clear().commit(); 287 } 288 setLocaleIndexed(Context context, String locale)289 static void setLocaleIndexed(Context context, String locale) { 290 context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) 291 .edit() 292 .putBoolean(locale, true) 293 .apply(); 294 } 295 setProvidersIndexed(Context context, String providerVersionedNames)296 static void setProvidersIndexed(Context context, String providerVersionedNames) { 297 context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) 298 .edit() 299 .putString(PREF_KEY_INDEXED_PROVIDERS, providerVersionedNames) 300 .apply(); 301 } 302 isLocaleAlreadyIndexed(Context context, String locale)303 static boolean isLocaleAlreadyIndexed(Context context, String locale) { 304 return context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).getBoolean(locale, false); 305 } 306 areProvidersIndexed(Context context, String providerVersionedNames)307 static boolean areProvidersIndexed(Context context, String providerVersionedNames) { 308 final String indexedProviders = context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) 309 .getString(PREF_KEY_INDEXED_PROVIDERS, null); 310 return TextUtils.equals(indexedProviders, providerVersionedNames); 311 } 312 isBuildIndexed(Context context, String buildNo)313 static boolean isBuildIndexed(Context context, String buildNo) { 314 return context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).getBoolean(buildNo, false); 315 } 316 setBuildIndexed(Context context, String buildNo)317 static void setBuildIndexed(Context context, String buildNo) { 318 context.getSharedPreferences(INDEX, 0).edit().putBoolean(buildNo, true).commit(); 319 } 320 dropTables(SQLiteDatabase db)321 private void dropTables(SQLiteDatabase db) { 322 clearCachedIndexed(mContext); 323 db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX); 324 db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX); 325 db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES); 326 db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SITE_MAP); 327 } 328 } 329