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 82 public PreIndexDataCollector(Context context) { 83 mContext = context; 84 } 85 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 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 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 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 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 205 Uri buildUriForXmlResources(String authority) { 206 return Uri.parse("content://" + authority + "/" + 207 SearchIndexablesContract.INDEXABLES_XML_RES_PATH); 208 } 209 210 @VisibleForTesting 211 Uri buildUriForRawData(String authority) { 212 return Uri.parse("content://" + authority + "/" + 213 SearchIndexablesContract.INDEXABLES_RAW_PATH); 214 } 215 216 @VisibleForTesting 217 Uri buildUriForNonIndexableKeys(String authority) { 218 return Uri.parse("content://" + authority + "/" + 219 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); 220 } 221 222 @VisibleForTesting 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 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 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 */ 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