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