Home | History | Annotate | Download | only in indexing
      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