blob: 51cc22189cf3dff9eccf17a73f8e5f78a936323f [file] [log] [blame]
/*
* 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 = 119;
@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 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.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<ResolveInfo> 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<ResolveInfo> 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<ResolveInfo> 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);
}
}