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 android.content.Context; 21 import android.content.res.Resources; 22 import android.content.res.XmlResourceParser; 23 import android.provider.SearchIndexableData; 24 import android.provider.SearchIndexableResource; 25 import androidx.annotation.DrawableRes; 26 import androidx.annotation.Nullable; 27 import androidx.collection.ArraySet; 28 import android.text.TextUtils; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.util.Pair; 32 import android.util.Xml; 33 34 import com.android.settings.intelligence.search.ResultPayload; 35 import com.android.settings.intelligence.search.SearchFeatureProvider; 36 import com.android.settings.intelligence.search.SearchIndexableRaw; 37 import com.android.settings.intelligence.search.sitemap.SiteMapPair; 38 39 import org.xmlpull.v1.XmlPullParser; 40 import org.xmlpull.v1.XmlPullParserException; 41 42 import java.io.IOException; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.TreeMap; 49 50 /** 51 * Helper class to convert {@link PreIndexData} to {@link IndexData}. 52 */ 53 public class IndexDataConverter { 54 55 private static final String TAG = "IndexDataConverter"; 56 57 private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; 58 private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference"; 59 private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference"; 60 private static final List<String> SKIP_NODES = Arrays.asList("intent", "extra"); 61 62 private final Context mContext; 63 IndexDataConverter(Context context)64 public IndexDataConverter(Context context) { 65 mContext = context; 66 } 67 68 /** 69 * Return the collection of {@param preIndexData} converted into {@link IndexData}. 70 * 71 * @param preIndexData a collection of {@link SearchIndexableResource}, 72 * {@link SearchIndexableRaw} and non-indexable keys. 73 */ convertPreIndexDataToIndexData(PreIndexData preIndexData)74 public List<IndexData> convertPreIndexDataToIndexData(PreIndexData preIndexData) { 75 final long startConversion = System.currentTimeMillis(); 76 final Map<String, List<SearchIndexableData>> indexableDataMap = 77 preIndexData.getDataToUpdate(); 78 final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys(); 79 final List<IndexData> indexData = new ArrayList<>(); 80 81 for (Map.Entry<String, List<SearchIndexableData>> entry : indexableDataMap.entrySet()) { 82 final String authority = entry.getKey(); 83 final List<SearchIndexableData> indexableData = entry.getValue(); 84 85 for (SearchIndexableData data : indexableData) { 86 if (data instanceof SearchIndexableRaw) { 87 final SearchIndexableRaw rawData = (SearchIndexableRaw) data; 88 final Set<String> rawNonIndexableKeys = nonIndexableKeys.get(authority); 89 final IndexData convertedRaw = convertRaw(mContext, authority, rawData, 90 rawNonIndexableKeys); 91 if (convertedRaw != null) { 92 indexData.add(convertedRaw); 93 } 94 } else if (data instanceof SearchIndexableResource) { 95 final SearchIndexableResource sir = (SearchIndexableResource) data; 96 final Set<String> resourceNonIndexableKeys = 97 getNonIndexableKeysForResource(nonIndexableKeys, authority); 98 final List<IndexData> resourceData = convertResource(sir, authority, 99 resourceNonIndexableKeys); 100 indexData.addAll(resourceData); 101 } 102 } 103 } 104 105 final long endConversion = System.currentTimeMillis(); 106 Log.d(TAG, "Converting pre-index data to index data took: " 107 + (endConversion - startConversion)); 108 109 return indexData; 110 } 111 112 /** 113 * Returns a full list of site map pairs based on metadata from all data sources. 114 * 115 * The content schema follows {@link IndexDatabaseHelper.Tables#TABLE_SITE_MAP} 116 */ convertSiteMapPairs(List<IndexData> indexData, List<Pair<String, String>> siteMapClassNames)117 public List<SiteMapPair> convertSiteMapPairs(List<IndexData> indexData, 118 List<Pair<String, String>> siteMapClassNames) { 119 final List<SiteMapPair> pairs = new ArrayList<>(); 120 if (indexData == null) { 121 return pairs; 122 } 123 // Step 1: loop indexData and build all static site map pairs. 124 final Map<String, String> classToTitleMap = new TreeMap<>(); 125 for (IndexData row : indexData) { 126 if (TextUtils.isEmpty(row.className)) { 127 continue; 128 } 129 // Build a map of [class, title] for the next step. 130 classToTitleMap.put(row.className, row.screenTitle); 131 if (!TextUtils.isEmpty(row.childClassName)) { 132 pairs.add(new SiteMapPair(row.className, row.screenTitle, 133 row.childClassName, row.updatedTitle)); 134 } 135 } 136 // Step 2: Extend the sitemap pairs by adding dynamic pairs provided by 137 // SearchIndexableProvider. The provider only tells us class name so we need to finish 138 // the mapping by looking up display title for each class. 139 for (Pair<String, String> pair : siteMapClassNames) { 140 final String parentName = classToTitleMap.get(pair.first); 141 final String childName = classToTitleMap.get(pair.second); 142 if (TextUtils.isEmpty(parentName) || TextUtils.isEmpty(childName)) { 143 Log.w(TAG, "Cannot build sitemap pair for incomplete names " 144 + pair + parentName + childName); 145 } else { 146 pairs.add(new SiteMapPair(pair.first, parentName, pair.second, childName)); 147 } 148 } 149 // Done 150 return pairs; 151 } 152 153 /** 154 * Return the conversion of {@link SearchIndexableRaw} to {@link IndexData}. 155 * The fields of {@link SearchIndexableRaw} are a subset of {@link IndexData}, 156 * and there is some data sanitization in the conversion. 157 */ 158 @Nullable convertRaw(Context context, String authority, SearchIndexableRaw raw, Set<String> nonIndexableKeys)159 private IndexData convertRaw(Context context, String authority, SearchIndexableRaw raw, 160 Set<String> nonIndexableKeys) { 161 if (TextUtils.isEmpty(raw.key)) { 162 Log.w(TAG, "Skipping null key for raw indexable " + authority + "/" + raw.title); 163 return null; 164 } 165 // A row is enabled if it does not show up as an nonIndexableKey 166 boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key)); 167 168 final IndexData.Builder builder = getIndexDataBuilder(); 169 builder.setTitle(raw.title) 170 .setSummaryOn(raw.summaryOn) 171 .setEntries(raw.entries) 172 .setKeywords(raw.keywords) 173 .setClassName(raw.className) 174 .setScreenTitle(raw.screenTitle) 175 .setIconResId(raw.iconResId) 176 .setIntentAction(raw.intentAction) 177 .setIntentTargetPackage(raw.intentTargetPackage) 178 .setIntentTargetClass(raw.intentTargetClass) 179 .setEnabled(enabled) 180 .setPackageName(raw.packageName) 181 .setAuthority(authority) 182 .setKey(raw.key); 183 184 return builder.build(context); 185 } 186 187 /** 188 * Return the conversion of the {@link SearchIndexableResource} to {@link IndexData}. 189 * Each of the elements in the xml layout attribute of {@param sir} is a candidate to be 190 * converted (including the header element). 191 * 192 * TODO (b/33577327) simplify this method. 193 */ convertResource(SearchIndexableResource sir, String authority, Set<String> nonIndexableKeys)194 private List<IndexData> convertResource(SearchIndexableResource sir, String authority, 195 Set<String> nonIndexableKeys) { 196 final Context context = sir.context; 197 XmlResourceParser parser = null; 198 199 List<IndexData> resourceIndexData = new ArrayList<>(); 200 try { 201 parser = context.getResources().getXml(sir.xmlResId); 202 203 int type; 204 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 205 && type != XmlPullParser.START_TAG) { 206 // Parse next until start tag is found 207 } 208 209 String nodeName = parser.getName(); 210 if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { 211 throw new RuntimeException( 212 "XML document must start with <PreferenceScreen> tag; found" 213 + nodeName + " at " + parser.getPositionDescription()); 214 } 215 216 final int outerDepth = parser.getDepth(); 217 final AttributeSet attrs = Xml.asAttributeSet(parser); 218 219 final String screenTitle = XmlParserUtils.getDataTitle(context, attrs); 220 final String headerKey = XmlParserUtils.getDataKey(context, attrs); 221 222 String title; 223 String key; 224 String headerTitle; 225 String summary; 226 String headerSummary; 227 String keywords; 228 String headerKeywords; 229 String childFragment; 230 @DrawableRes int iconResId; 231 ResultPayload payload; 232 boolean enabled; 233 234 // TODO REFACTOR (b/62807132) Add proper inline support 235 // Map<String, PreferenceControllerMixin> controllerUriMap = null; 236 // 237 // if (fragmentName != null) { 238 // controllerUriMap = DatabaseIndexingUtils 239 // .getPreferenceControllerUriMap(fragmentName, context); 240 // } 241 242 headerTitle = XmlParserUtils.getDataTitle(context, attrs); 243 headerSummary = XmlParserUtils.getDataSummary(context, attrs); 244 headerKeywords = XmlParserUtils.getDataKeywords(context, attrs); 245 enabled = !nonIndexableKeys.contains(headerKey); 246 // TODO: Set payload type for header results 247 IndexData.Builder headerBuilder = getIndexDataBuilder(); 248 headerBuilder.setTitle(headerTitle) 249 .setSummaryOn(headerSummary) 250 .setScreenTitle(screenTitle) 251 .setKeywords(headerKeywords) 252 .setClassName(sir.className) 253 .setPackageName(sir.packageName) 254 .setAuthority(authority) 255 .setIntentAction(sir.intentAction) 256 .setIntentTargetPackage(sir.intentTargetPackage) 257 .setIntentTargetClass(sir.intentTargetClass) 258 .setEnabled(enabled) 259 .setKey(headerKey); 260 261 // Flag for XML headers which a child element's title. 262 boolean isHeaderUnique = true; 263 IndexData.Builder builder; 264 265 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 266 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 267 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 268 continue; 269 } 270 271 nodeName = parser.getName(); 272 if (SKIP_NODES.contains(nodeName)) { 273 if (SearchFeatureProvider.DEBUG) { 274 Log.d(TAG, nodeName + " is not a valid entity to index, skip."); 275 } 276 continue; 277 } 278 279 title = XmlParserUtils.getDataTitle(context, attrs); 280 key = XmlParserUtils.getDataKey(context, attrs); 281 enabled = !nonIndexableKeys.contains(key); 282 keywords = XmlParserUtils.getDataKeywords(context, attrs); 283 iconResId = XmlParserUtils.getDataIcon(context, attrs); 284 285 if (isHeaderUnique && TextUtils.equals(headerTitle, title)) { 286 isHeaderUnique = false; 287 } 288 289 builder = getIndexDataBuilder(); 290 builder.setTitle(title) 291 .setKeywords(keywords) 292 .setClassName(sir.className) 293 .setScreenTitle(screenTitle) 294 .setIconResId(iconResId) 295 .setPackageName(sir.packageName) 296 .setAuthority(authority) 297 .setIntentAction(sir.intentAction) 298 .setIntentTargetPackage(sir.intentTargetPackage) 299 .setIntentTargetClass(sir.intentTargetClass) 300 .setEnabled(enabled) 301 .setKey(key); 302 303 if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { 304 summary = XmlParserUtils.getDataSummary(context, attrs); 305 306 String entries = null; 307 308 if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { 309 entries = XmlParserUtils.getDataEntries(context, attrs); 310 } 311 312 // TODO (b/62254931) index primitives instead of payload 313 // TODO (b/62807132) Add proper inline support 314 //payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key); 315 childFragment = XmlParserUtils.getDataChildFragment(context, attrs); 316 317 builder.setSummaryOn(summary) 318 .setEntries(entries) 319 .setChildClassName(childFragment); 320 tryAddIndexDataToList(resourceIndexData, builder); 321 } else { 322 // TODO (b/33577327) We removed summary off here. We should check if we can 323 // merge this 'else' section with the one above. Put a break point to 324 // investigate. 325 String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs); 326 327 if (TextUtils.isEmpty(summaryOn)) { 328 summaryOn = XmlParserUtils.getDataSummary(context, attrs); 329 } 330 331 builder.setSummaryOn(summaryOn); 332 333 tryAddIndexDataToList(resourceIndexData, builder); 334 } 335 } 336 337 // The xml header's title does not match the title of one of the child settings. 338 if (isHeaderUnique) { 339 tryAddIndexDataToList(resourceIndexData, headerBuilder); 340 } 341 } catch (XmlPullParserException e) { 342 Log.w(TAG, "XML Error parsing PreferenceScreen: " + sir.className, e); 343 } catch (IOException e) { 344 Log.w(TAG, "IO Error parsing PreferenceScreen: " + sir.className, e); 345 } catch (Resources.NotFoundException e) { 346 Log.w(TAG, "Resoucre not found error parsing PreferenceScreen: " + sir.className, e); 347 } finally { 348 if (parser != null) { 349 parser.close(); 350 } 351 } 352 return resourceIndexData; 353 } 354 tryAddIndexDataToList(List<IndexData> list, IndexData.Builder data)355 private void tryAddIndexDataToList(List<IndexData> list, IndexData.Builder data) { 356 if (!TextUtils.isEmpty(data.getKey())) { 357 list.add(data.build(mContext)); 358 } else { 359 Log.w(TAG, "Skipping index for null-key item " + data); 360 } 361 } 362 getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys, String authority)363 private Set<String> getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys, 364 String authority) { 365 final Set<String> result = nonIndexableKeys.get(authority); 366 return result != null ? result : new ArraySet<>(); 367 } 368 getIndexDataBuilder()369 protected IndexData.Builder getIndexDataBuilder() { 370 return new IndexData.Builder(); 371 } 372 } 373