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