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 
18 package com.android.settings.intelligence.search.indexing;
19 
20 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
21 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
22 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
23 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
24 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
25 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
26 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
27 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
28 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
29 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
30 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
31 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
32 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
33 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
34 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
35 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
36 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
37 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
38 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
39 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
40 
41 import android.Manifest;
42 import android.content.ContentResolver;
43 import android.content.Context;
44 import android.content.pm.ApplicationInfo;
45 import android.content.pm.PackageInfo;
46 import android.content.pm.PackageManager;
47 import android.content.pm.ResolveInfo;
48 import android.database.Cursor;
49 import android.net.Uri;
50 import android.provider.SearchIndexableResource;
51 import android.provider.SearchIndexablesContract;
52 import androidx.annotation.VisibleForTesting;
53 import android.text.TextUtils;
54 import android.util.ArraySet;
55 import android.util.Log;
56 import android.util.Pair;
57 
58 import com.android.settings.intelligence.search.SearchFeatureProvider;
59 import com.android.settings.intelligence.search.SearchIndexableRaw;
60 
61 import java.util.ArrayList;
62 import java.util.Collections;
63 import java.util.List;
64 import java.util.Set;
65 
66 /**
67  * Collects all data from {@link android.provider.SearchIndexablesProvider} to be indexed.
68  */
69 public class PreIndexDataCollector {
70 
71     private static final String TAG = "IndexableDataCollector";
72 
73     private static final List<String> EMPTY_LIST = Collections.emptyList();
74 
75     private Context mContext;
76 
77     private PreIndexData mIndexData;
78 
PreIndexDataCollector(Context context)79     public PreIndexDataCollector(Context context) {
80         mContext = context;
81     }
82 
collectIndexableData(List<ResolveInfo> providers, boolean isFullIndex)83     public PreIndexData collectIndexableData(List<ResolveInfo> providers, boolean isFullIndex) {
84         mIndexData = new PreIndexData();
85 
86         for (final ResolveInfo info : providers) {
87             if (!isWellKnownProvider(info)) {
88                 continue;
89             }
90             final String authority = info.providerInfo.authority;
91             final String packageName = info.providerInfo.packageName;
92 
93             if (isFullIndex) {
94                 addIndexablesFromRemoteProvider(packageName, authority);
95             }
96 
97             final long nonIndexableStartTime = System.currentTimeMillis();
98             addNonIndexablesKeysFromRemoteProvider(packageName, authority);
99             if (SearchFeatureProvider.DEBUG) {
100                 final long nonIndexableTime = System.currentTimeMillis() - nonIndexableStartTime;
101                 Log.d(TAG, "performIndexing update non-indexable for package " + packageName
102                         + " took time: " + nonIndexableTime);
103             }
104         }
105 
106         return mIndexData;
107     }
108 
addIndexablesFromRemoteProvider(String packageName, String authority)109     private void addIndexablesFromRemoteProvider(String packageName, String authority) {
110         try {
111             final Context context = mContext.createPackageContext(packageName, 0);
112 
113             final Uri uriForResources = buildUriForXmlResources(authority);
114             mIndexData.addDataToUpdate(authority, getIndexablesForXmlResourceUri(
115                     context, packageName, uriForResources,
116                     SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS));
117 
118             final Uri uriForRawData = buildUriForRawData(authority);
119             mIndexData.addDataToUpdate(authority, getIndexablesForRawDataUri(
120                     context, packageName, uriForRawData,
121                     SearchIndexablesContract.INDEXABLES_RAW_COLUMNS));
122 
123             final Uri uriForSiteMap = buildUriForSiteMap(authority);
124             mIndexData.addSiteMapPairs(getSiteMapFromProvider(context, uriForSiteMap));
125         } catch (PackageManager.NameNotFoundException e) {
126             Log.w(TAG, "Could not create context for " + packageName + ": "
127                     + Log.getStackTraceString(e));
128         }
129     }
130 
131     @VisibleForTesting
getIndexablesForXmlResourceUri(Context packageContext, String packageName, Uri uri, String[] projection)132     List<SearchIndexableResource> getIndexablesForXmlResourceUri(Context packageContext,
133             String packageName, Uri uri, String[] projection) {
134 
135         final ContentResolver resolver = packageContext.getContentResolver();
136         final Cursor cursor = resolver.query(uri, projection, null, null, null);
137         List<SearchIndexableResource> resources = new ArrayList<>();
138 
139         if (cursor == null) {
140             Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
141             return resources;
142         }
143 
144         try {
145             final int count = cursor.getCount();
146             if (count > 0) {
147                 while (cursor.moveToNext()) {
148                     SearchIndexableResource sir = new SearchIndexableResource(packageContext);
149                     sir.packageName = packageName;
150                     sir.xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
151                     sir.className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
152                     sir.iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
153                     sir.intentAction = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
154                     sir.intentTargetPackage = cursor.getString(
155                             COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
156                     sir.intentTargetClass = cursor.getString(
157                             COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
158                     resources.add(sir);
159                 }
160             }
161         } finally {
162             cursor.close();
163         }
164         return resources;
165     }
166 
addNonIndexablesKeysFromRemoteProvider(String packageName, String authority)167     private void addNonIndexablesKeysFromRemoteProvider(String packageName, String authority) {
168         final List<String> keys =
169                 getNonIndexablesKeysFromRemoteProvider(packageName, authority);
170 
171         if (keys != null && !keys.isEmpty()) {
172             Set<String> keySet = new ArraySet<>();
173             keySet.addAll(keys);
174             mIndexData.addNonIndexableKeysForAuthority(authority, keySet);
175         }
176     }
177 
178     @VisibleForTesting
getNonIndexablesKeysFromRemoteProvider(String packageName, String authority)179     List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
180             String authority) {
181         try {
182             final Context packageContext = mContext.createPackageContext(packageName, 0);
183 
184             final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority);
185             return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys,
186                     SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
187         } catch (PackageManager.NameNotFoundException e) {
188             Log.w(TAG, "Could not create context for " + packageName + ": "
189                     + Log.getStackTraceString(e));
190             return EMPTY_LIST;
191         }
192     }
193 
buildUriForXmlResources(String authority)194     private Uri buildUriForXmlResources(String authority) {
195         return Uri.parse("content://" + authority + "/" +
196                 SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
197     }
198 
buildUriForRawData(String authority)199     private Uri buildUriForRawData(String authority) {
200         return Uri.parse("content://" + authority + "/" +
201                 SearchIndexablesContract.INDEXABLES_RAW_PATH);
202     }
203 
buildUriForNonIndexableKeys(String authority)204     private Uri buildUriForNonIndexableKeys(String authority) {
205         return Uri.parse("content://" + authority + "/" +
206                 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
207     }
208 
209     @VisibleForTesting
buildUriForSiteMap(String authority)210     Uri buildUriForSiteMap(String authority) {
211         return Uri.parse("content://" + authority + "/settings/site_map_pairs");
212     }
213 
214     @VisibleForTesting
getIndexablesForRawDataUri(Context packageContext, String packageName, Uri uri, String[] projection)215     List<SearchIndexableRaw> getIndexablesForRawDataUri(Context packageContext, String packageName,
216             Uri uri, String[] projection) {
217         final ContentResolver resolver = packageContext.getContentResolver();
218         final Cursor cursor = resolver.query(uri, projection, null, null, null);
219         List<SearchIndexableRaw> rawData = new ArrayList<>();
220 
221         if (cursor == null) {
222             Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
223             return rawData;
224         }
225 
226         try {
227             final int count = cursor.getCount();
228             if (count > 0) {
229                 while (cursor.moveToNext()) {
230                     final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
231                     final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
232                     final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
233                     final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
234                     final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
235 
236                     final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
237 
238                     final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
239                     final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
240 
241                     final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
242                     final String targetPackage = cursor.getString(
243                             COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
244                     final String targetClass = cursor.getString(
245                             COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
246 
247                     final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
248                     final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
249 
250                     SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
251                     data.title = title;
252                     data.summaryOn = summaryOn;
253                     data.summaryOff = summaryOff;
254                     data.entries = entries;
255                     data.keywords = keywords;
256                     data.screenTitle = screenTitle;
257                     data.className = className;
258                     data.packageName = packageName;
259                     data.iconResId = iconResId;
260                     data.intentAction = action;
261                     data.intentTargetPackage = targetPackage;
262                     data.intentTargetClass = targetClass;
263                     data.key = key;
264                     data.userId = userId;
265 
266                     rawData.add(data);
267                 }
268             }
269         } finally {
270             cursor.close();
271         }
272 
273         return rawData;
274     }
275 
276     @VisibleForTesting
getSiteMapFromProvider(Context packageContext, Uri uri)277     List<Pair<String, String>> getSiteMapFromProvider(Context packageContext, Uri uri) {
278         final ContentResolver resolver = packageContext.getContentResolver();
279         final Cursor cursor = resolver.query(uri, null, null, null, null);
280         if (cursor == null) {
281             Log.d(TAG, "No site map information from " + packageContext.getPackageName());
282             return null;
283         }
284         final List<Pair<String, String>> siteMapPairs = new ArrayList<>();
285         try {
286             final int count = cursor.getCount();
287             if (count > 0) {
288                 while (cursor.moveToNext()) {
289                     final String parentClass = cursor.getString(cursor.getColumnIndex(
290                             IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS));
291                     final String childClass = cursor.getString(cursor.getColumnIndex(
292                             IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS));
293                     if (TextUtils.isEmpty(parentClass)  || TextUtils.isEmpty(childClass)) {
294                         Log.w(TAG, "Incomplete site map pair: " + parentClass + "/" + childClass);
295                         continue;
296                     }
297                     siteMapPairs.add(Pair.create(parentClass, childClass));
298                 }
299             }
300             return siteMapPairs;
301         } finally {
302             cursor.close();
303         }
304 
305     }
306 
getNonIndexablesKeys(Context packageContext, Uri uri, String[] projection)307     private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
308             String[] projection) {
309 
310         final ContentResolver resolver = packageContext.getContentResolver();
311         final Cursor cursor = resolver.query(uri, projection, null, null, null);
312         final List<String> result = new ArrayList<>();
313 
314         if (cursor == null) {
315             Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
316             return result;
317         }
318 
319         try {
320             final int count = cursor.getCount();
321             if (count > 0) {
322                 while (cursor.moveToNext()) {
323                     final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
324 
325                     if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) {
326                         Log.v(TAG, "Empty non-indexable key from: "
327                                 + packageContext.getPackageName());
328                         continue;
329                     }
330 
331                     result.add(key);
332                 }
333             }
334             return result;
335         } finally {
336             cursor.close();
337         }
338     }
339 
340     /**
341      * Only allow a "well known" SearchIndexablesProvider. The provider should:
342      *
343      * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES}
344      * - be from a privileged package
345      */
346     @VisibleForTesting
isWellKnownProvider(ResolveInfo info)347     boolean isWellKnownProvider(ResolveInfo info) {
348         final String authority = info.providerInfo.authority;
349         final String packageName = info.providerInfo.applicationInfo.packageName;
350 
351         if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) {
352             return false;
353         }
354 
355         final String readPermission = info.providerInfo.readPermission;
356         final String writePermission = info.providerInfo.writePermission;
357 
358         if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) {
359             return false;
360         }
361 
362         if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) ||
363                 !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) {
364             return false;
365         }
366 
367         return isPrivilegedPackage(packageName, mContext);
368     }
369 
370     /**
371      * @return true if the {@param packageName} is privileged.
372      */
isPrivilegedPackage(String packageName, Context context)373     private boolean isPrivilegedPackage(String packageName, Context context) {
374         final PackageManager pm = context.getPackageManager();
375         try {
376             PackageInfo packInfo = pm.getPackageInfo(packageName, 0);
377             // TODO REFACTOR Changed privileged check
378             return ((packInfo.applicationInfo.flags
379                     & ApplicationInfo.FLAG_SYSTEM) != 0);
380         } catch (PackageManager.NameNotFoundException e) {
381             return false;
382         }
383     }
384 }
385