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