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