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