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 android.support.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 79 public PreIndexDataCollector(Context context) { 80 mContext = context; 81 } 82 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 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(getIndexablesForXmlResourceUri(context, packageName, 115 uriForResources, SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS)); 116 117 final Uri uriForRawData = buildUriForRawData(authority); 118 mIndexData.addDataToUpdate(getIndexablesForRawDataUri(context, packageName, 119 uriForRawData, SearchIndexablesContract.INDEXABLES_RAW_COLUMNS)); 120 121 final Uri uriForSiteMap = buildUriForSiteMap(authority); 122 mIndexData.addSiteMapPairs(getSiteMapFromProvider(context, uriForSiteMap)); 123 } catch (PackageManager.NameNotFoundException e) { 124 Log.w(TAG, "Could not create context for " + packageName + ": " 125 + Log.getStackTraceString(e)); 126 } 127 } 128 129 @VisibleForTesting 130 List<SearchIndexableResource> getIndexablesForXmlResourceUri(Context packageContext, 131 String packageName, Uri uri, String[] projection) { 132 133 final ContentResolver resolver = packageContext.getContentResolver(); 134 final Cursor cursor = resolver.query(uri, projection, null, null, null); 135 List<SearchIndexableResource> resources = new ArrayList<>(); 136 137 if (cursor == null) { 138 Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); 139 return resources; 140 } 141 142 try { 143 final int count = cursor.getCount(); 144 if (count > 0) { 145 while (cursor.moveToNext()) { 146 SearchIndexableResource sir = new SearchIndexableResource(packageContext); 147 sir.packageName = packageName; 148 sir.xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); 149 sir.className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); 150 sir.iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); 151 sir.intentAction = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); 152 sir.intentTargetPackage = cursor.getString( 153 COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); 154 sir.intentTargetClass = cursor.getString( 155 COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); 156 resources.add(sir); 157 } 158 } 159 } finally { 160 cursor.close(); 161 } 162 return resources; 163 } 164 165 private void addNonIndexablesKeysFromRemoteProvider(String packageName, String authority) { 166 final List<String> keys = 167 getNonIndexablesKeysFromRemoteProvider(packageName, authority); 168 169 if (keys != null && !keys.isEmpty()) { 170 Set<String> keySet = new ArraySet<>(); 171 keySet.addAll(keys); 172 mIndexData.addNonIndexableKeysForAuthority(authority, keySet); 173 } 174 } 175 176 @VisibleForTesting 177 List<String> getNonIndexablesKeysFromRemoteProvider(String packageName, 178 String authority) { 179 try { 180 final Context packageContext = mContext.createPackageContext(packageName, 0); 181 182 final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority); 183 return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys, 184 SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS); 185 } catch (PackageManager.NameNotFoundException e) { 186 Log.w(TAG, "Could not create context for " + packageName + ": " 187 + Log.getStackTraceString(e)); 188 return EMPTY_LIST; 189 } 190 } 191 192 private Uri buildUriForXmlResources(String authority) { 193 return Uri.parse("content://" + authority + "/" + 194 SearchIndexablesContract.INDEXABLES_XML_RES_PATH); 195 } 196 197 private Uri buildUriForRawData(String authority) { 198 return Uri.parse("content://" + authority + "/" + 199 SearchIndexablesContract.INDEXABLES_RAW_PATH); 200 } 201 202 private Uri buildUriForNonIndexableKeys(String authority) { 203 return Uri.parse("content://" + authority + "/" + 204 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); 205 } 206 207 @VisibleForTesting 208 Uri buildUriForSiteMap(String authority) { 209 return Uri.parse("content://" + authority + "/settings/site_map_pairs"); 210 } 211 212 @VisibleForTesting 213 List<SearchIndexableRaw> getIndexablesForRawDataUri(Context packageContext, String packageName, 214 Uri uri, String[] projection) { 215 final ContentResolver resolver = packageContext.getContentResolver(); 216 final Cursor cursor = resolver.query(uri, projection, null, null, null); 217 List<SearchIndexableRaw> rawData = new ArrayList<>(); 218 219 if (cursor == null) { 220 Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); 221 return rawData; 222 } 223 224 try { 225 final int count = cursor.getCount(); 226 if (count > 0) { 227 while (cursor.moveToNext()) { 228 final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); 229 final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); 230 final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); 231 final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); 232 final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); 233 234 final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); 235 236 final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); 237 final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); 238 239 final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); 240 final String targetPackage = cursor.getString( 241 COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); 242 final String targetClass = cursor.getString( 243 COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); 244 245 final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); 246 final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); 247 248 SearchIndexableRaw data = new SearchIndexableRaw(packageContext); 249 data.title = title; 250 data.summaryOn = summaryOn; 251 data.summaryOff = summaryOff; 252 data.entries = entries; 253 data.keywords = keywords; 254 data.screenTitle = screenTitle; 255 data.className = className; 256 data.packageName = packageName; 257 data.iconResId = iconResId; 258 data.intentAction = action; 259 data.intentTargetPackage = targetPackage; 260 data.intentTargetClass = targetClass; 261 data.key = key; 262 data.userId = userId; 263 264 rawData.add(data); 265 } 266 } 267 } finally { 268 cursor.close(); 269 } 270 271 return rawData; 272 } 273 274 @VisibleForTesting 275 List<Pair<String, String>> getSiteMapFromProvider(Context packageContext, Uri uri) { 276 final ContentResolver resolver = packageContext.getContentResolver(); 277 final Cursor cursor = resolver.query(uri, null, null, null, null); 278 if (cursor == null) { 279 Log.d(TAG, "No site map information from " + packageContext.getPackageName()); 280 return null; 281 } 282 final List<Pair<String, String>> siteMapPairs = new ArrayList<>(); 283 try { 284 final int count = cursor.getCount(); 285 if (count > 0) { 286 while (cursor.moveToNext()) { 287 final String parentClass = cursor.getString(cursor.getColumnIndex( 288 IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS)); 289 final String childClass = cursor.getString(cursor.getColumnIndex( 290 IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS)); 291 if (TextUtils.isEmpty(parentClass) || TextUtils.isEmpty(childClass)) { 292 Log.w(TAG, "Incomplete site map pair: " + parentClass + "/" + childClass); 293 continue; 294 } 295 siteMapPairs.add(Pair.create(parentClass, childClass)); 296 } 297 } 298 return siteMapPairs; 299 } finally { 300 cursor.close(); 301 } 302 303 } 304 305 private List<String> getNonIndexablesKeys(Context packageContext, Uri uri, 306 String[] projection) { 307 308 final ContentResolver resolver = packageContext.getContentResolver(); 309 final Cursor cursor = resolver.query(uri, projection, null, null, null); 310 final List<String> result = new ArrayList<>(); 311 312 if (cursor == null) { 313 Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); 314 return result; 315 } 316 317 try { 318 final int count = cursor.getCount(); 319 if (count > 0) { 320 while (cursor.moveToNext()) { 321 final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE); 322 323 if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) { 324 Log.v(TAG, "Empty non-indexable key from: " 325 + packageContext.getPackageName()); 326 continue; 327 } 328 329 result.add(key); 330 } 331 } 332 return result; 333 } finally { 334 cursor.close(); 335 } 336 } 337 338 /** 339 * Only allow a "well known" SearchIndexablesProvider. The provider should: 340 * 341 * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES} 342 * - be from a privileged package 343 */ 344 @VisibleForTesting 345 boolean isWellKnownProvider(ResolveInfo info) { 346 final String authority = info.providerInfo.authority; 347 final String packageName = info.providerInfo.applicationInfo.packageName; 348 349 if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { 350 return false; 351 } 352 353 final String readPermission = info.providerInfo.readPermission; 354 final String writePermission = info.providerInfo.writePermission; 355 356 if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { 357 return false; 358 } 359 360 if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || 361 !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { 362 return false; 363 } 364 365 return isPrivilegedPackage(packageName, mContext); 366 } 367 368 /** 369 * @return true if the {@param packageName} is privileged. 370 */ 371 private boolean isPrivilegedPackage(String packageName, Context context) { 372 final PackageManager pm = context.getPackageManager(); 373 try { 374 PackageInfo packInfo = pm.getPackageInfo(packageName, 0); 375 // TODO REFACTOR Changed privileged check 376 return ((packInfo.applicationInfo.flags 377 & ApplicationInfo.FLAG_SYSTEM) != 0); 378 } catch (PackageManager.NameNotFoundException e) { 379 return false; 380 } 381 } 382 } 383