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