/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.intelligence.search.indexing; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import java.util.List; import java.util.Locale; public class IndexDatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "IndexDatabaseHelper"; private static final String DATABASE_NAME = "search_index.db"; private static final int DATABASE_VERSION = 120; @VisibleForTesting static final String SHARED_PREFS_TAG = "indexing_manager"; private static final String PREF_KEY_INDEXED_PROVIDERS = "indexed_providers"; public interface Tables { String TABLE_PREFS_INDEX = "prefs_index"; String TABLE_SITE_MAP = "site_map"; String TABLE_META_INDEX = "meta_index"; String TABLE_SAVED_QUERIES = "saved_queries"; } public interface IndexColumns { String DATA_TITLE = "data_title"; String DATA_TITLE_NORMALIZED = "data_title_normalized"; String DATA_SUMMARY_ON = "data_summary_on"; String DATA_SUMMARY_ON_NORMALIZED = "data_summary_on_normalized"; String DATA_SUMMARY_OFF = "data_summary_off"; String DATA_SUMMARY_OFF_NORMALIZED = "data_summary_off_normalized"; String DATA_ENTRIES = "data_entries"; String DATA_KEYWORDS = "data_keywords"; String DATA_PACKAGE = "package"; String DATA_AUTHORITY = "authority"; String CLASS_NAME = "class_name"; String SCREEN_TITLE = "screen_title"; String INTENT_ACTION = "intent_action"; String INTENT_TARGET_PACKAGE = "intent_target_package"; String INTENT_TARGET_CLASS = "intent_target_class"; String ICON = "icon"; String ENABLED = "enabled"; String DATA_KEY_REF = "data_key_reference"; String PAYLOAD_TYPE = "payload_type"; String PAYLOAD = "payload"; } public interface MetaColumns { String BUILD = "build"; } public interface SavedQueriesColumns { String QUERY = "query"; String TIME_STAMP = "timestamp"; } public interface SiteMapColumns { String DOCID = "docid"; String PARENT_CLASS = "parent_class"; String CHILD_CLASS = "child_class"; String PARENT_TITLE = "parent_title"; String CHILD_TITLE = "child_title"; } private static final String CREATE_INDEX_TABLE = "CREATE VIRTUAL TABLE " + Tables.TABLE_PREFS_INDEX + " USING fts4" + "(" + IndexColumns.DATA_TITLE + ", " + IndexColumns.DATA_TITLE_NORMALIZED + ", " + IndexColumns.DATA_SUMMARY_ON + ", " + IndexColumns.DATA_SUMMARY_ON_NORMALIZED + ", " + IndexColumns.DATA_SUMMARY_OFF + ", " + IndexColumns.DATA_SUMMARY_OFF_NORMALIZED + ", " + IndexColumns.DATA_ENTRIES + ", " + IndexColumns.DATA_KEYWORDS + ", " + IndexColumns.DATA_PACKAGE + ", " + IndexColumns.DATA_AUTHORITY + ", " + IndexColumns.SCREEN_TITLE + ", " + IndexColumns.CLASS_NAME + ", " + IndexColumns.ICON + ", " + IndexColumns.INTENT_ACTION + ", " + IndexColumns.INTENT_TARGET_PACKAGE + ", " + IndexColumns.INTENT_TARGET_CLASS + ", " + IndexColumns.ENABLED + ", " + IndexColumns.DATA_KEY_REF + ", " + IndexColumns.PAYLOAD_TYPE + ", " + IndexColumns.PAYLOAD + ");"; private static final String CREATE_META_TABLE = "CREATE TABLE " + Tables.TABLE_META_INDEX + "(" + MetaColumns.BUILD + " VARCHAR(32) NOT NULL" + ")"; private static final String CREATE_SAVED_QUERIES_TABLE = "CREATE TABLE " + Tables.TABLE_SAVED_QUERIES + "(" + SavedQueriesColumns.QUERY + " VARCHAR(64) NOT NULL" + ", " + SavedQueriesColumns.TIME_STAMP + " INTEGER" + ")"; private static final String CREATE_SITE_MAP_TABLE = "CREATE VIRTUAL TABLE " + Tables.TABLE_SITE_MAP + " USING fts4" + "(" + SiteMapColumns.PARENT_CLASS + ", " + SiteMapColumns.CHILD_CLASS + ", " + SiteMapColumns.PARENT_TITLE + ", " + SiteMapColumns.CHILD_TITLE + ")"; private static final String INSERT_BUILD_VERSION = "INSERT INTO " + Tables.TABLE_META_INDEX + " VALUES ('" + Build.VERSION.INCREMENTAL + "');"; private static final String SELECT_BUILD_VERSION = "SELECT " + MetaColumns.BUILD + " FROM " + Tables.TABLE_META_INDEX + " LIMIT 1;"; private static IndexDatabaseHelper sSingleton; private final Context mContext; public static synchronized IndexDatabaseHelper getInstance(Context context) { if (sSingleton == null) { sSingleton = new IndexDatabaseHelper(context); } return sSingleton; } public IndexDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context.getApplicationContext(); } @Override public void onCreate(SQLiteDatabase db) { bootstrapDB(db); } private void bootstrapDB(SQLiteDatabase db) { db.execSQL(CREATE_INDEX_TABLE); db.execSQL(CREATE_META_TABLE); db.execSQL(CREATE_SAVED_QUERIES_TABLE); db.execSQL(CREATE_SITE_MAP_TABLE); db.execSQL(INSERT_BUILD_VERSION); Log.i(TAG, "Bootstrapped database"); } @Override public void onOpen(SQLiteDatabase db) { super.onOpen(db); Log.i(TAG, "Using schema version: " + db.getVersion()); if (!Build.VERSION.INCREMENTAL.equals(getBuildVersion(db))) { Log.w(TAG, "Index needs to be rebuilt as build-version is not the same"); // We need to drop the tables and recreate them reconstruct(db); } else { Log.i(TAG, "Index is fine"); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < DATABASE_VERSION) { Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + "Index needs to be rebuilt for schema version '" + newVersion + "'."); // We need to drop the tables and recreate them reconstruct(db); } } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Detected schema version '" + oldVersion + "'. " + "Index needs to be rebuilt for schema version '" + newVersion + "'."); // We need to drop the tables and recreate them reconstruct(db); } public void reconstruct(SQLiteDatabase db) { mContext.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) .edit() .clear() .commit(); dropTables(db); bootstrapDB(db); } private String getBuildVersion(SQLiteDatabase db) { String version = null; Cursor cursor = null; try { cursor = db.rawQuery(SELECT_BUILD_VERSION, null); if (cursor.moveToFirst()) { version = cursor.getString(0); } } catch (Exception e) { Log.e(TAG, "Cannot get build version from Index metadata"); } finally { if (cursor != null) { cursor.close(); } } return version; } @VisibleForTesting static String buildProviderVersionedNames(Context context, List providers) { // TODO Refactor update test to reflect version code change. try { StringBuilder sb = new StringBuilder(); for (ResolveInfo info : providers) { String packageName = info.providerInfo.packageName; PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, 0 /* flags */); sb.append(packageName) .append(':') .append(packageInfo.versionCode) .append(','); } // add SettingsIntelligence version as well. sb.append(context.getPackageName()) .append(':') .append(context.getPackageManager() .getPackageInfo(context.getPackageName(), 0 /* flags */).versionCode); return sb.toString(); } catch (PackageManager.NameNotFoundException e) { Log.d(TAG, "Could not find package name in provider", e); } return ""; } /** * Set a flag that indicates the search database is fully indexed. */ static void setIndexed(Context context, List providers) { final String localeStr = Locale.getDefault().toString(); final String fingerprint = Build.FINGERPRINT; final String providerVersionedNames = IndexDatabaseHelper.buildProviderVersionedNames(context, providers); context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) .edit() .putBoolean(localeStr, true) .putBoolean(fingerprint, true) .putString(PREF_KEY_INDEXED_PROVIDERS, providerVersionedNames) .apply(); } /** * Checks if the indexed data requires full index. The index data is out of date when: * - Device language has changed * - Device has taken an OTA. * In both cases, the device requires a full index. * * @return true if a full index should be preformed. */ static boolean isFullIndex(Context context, List providers) { final String localeStr = Locale.getDefault().toString(); final String fingerprint = Build.FINGERPRINT; final String providerVersionedNames = IndexDatabaseHelper.buildProviderVersionedNames(context, providers); final SharedPreferences prefs = context .getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE); final boolean isIndexed = prefs.getBoolean(fingerprint, false) && prefs.getBoolean(localeStr, false) && TextUtils.equals( prefs.getString(PREF_KEY_INDEXED_PROVIDERS, null), providerVersionedNames); return !isIndexed; } private void dropTables(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SITE_MAP); } }