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