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