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 18package com.android.settings.search.indexing; 19 20import android.annotation.Nullable; 21import android.content.Context; 22import android.content.res.Resources; 23import android.content.res.XmlResourceParser; 24import android.provider.SearchIndexableData; 25import android.provider.SearchIndexableResource; 26import android.support.annotation.DrawableRes; 27import android.text.TextUtils; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.util.Xml; 31 32import com.android.settings.search.DatabaseIndexingUtils; 33import com.android.settings.core.PreferenceXmlParserUtils; 34import com.android.settings.search.ResultPayload; 35import com.android.settings.search.SearchIndexableRaw; 36 37import org.xmlpull.v1.XmlPullParser; 38import org.xmlpull.v1.XmlPullParserException; 39 40import java.io.IOException; 41import java.util.ArrayList; 42import java.util.HashMap; 43import java.util.HashSet; 44import java.util.List; 45import java.util.Map; 46import java.util.Set; 47 48/** 49 * Helper class to convert {@link PreIndexData} to {@link IndexData}. 50 */ 51public class IndexDataConverter { 52 53 private static final String LOG_TAG = "IndexDataConverter"; 54 55 private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; 56 private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference"; 57 private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference"; 58 59 private final Context mContext; 60 61 public IndexDataConverter(Context context) { 62 mContext = context; 63 } 64 65 /** 66 * Return the collection of {@param preIndexData} converted into {@link IndexData}. 67 * 68 * @param preIndexData a collection of {@link SearchIndexableResource}, 69 * {@link SearchIndexableRaw} and non-indexable keys. 70 */ 71 public List<IndexData> convertPreIndexDataToIndexData(PreIndexData preIndexData) { 72 final long current = System.currentTimeMillis(); 73 final List<SearchIndexableData> indexableData = preIndexData.dataToUpdate; 74 final Map<String, Set<String>> nonIndexableKeys = preIndexData.nonIndexableKeys; 75 final List<IndexData> indexData = new ArrayList<>(); 76 77 for (SearchIndexableData data : indexableData) { 78 if (data instanceof SearchIndexableRaw) { 79 final SearchIndexableRaw rawData = (SearchIndexableRaw) data; 80 final Set<String> rawNonIndexableKeys = nonIndexableKeys.get( 81 rawData.intentTargetPackage); 82 final IndexData.Builder builder = convertRaw(rawData, rawNonIndexableKeys); 83 84 if (builder != null) { 85 indexData.add(builder.build(mContext)); 86 } 87 } else if (data instanceof SearchIndexableResource) { 88 final SearchIndexableResource sir = (SearchIndexableResource) data; 89 final Set<String> resourceNonIndexableKeys = 90 getNonIndexableKeysForResource(nonIndexableKeys, sir.packageName); 91 final List<IndexData> resourceData = convertResource(sir, resourceNonIndexableKeys); 92 indexData.addAll(resourceData); 93 } 94 } 95 96 final long endConversion = System.currentTimeMillis(); 97 Log.d(LOG_TAG, "Converting pre-index data to index data took: " 98 + (endConversion - current)); 99 100 return indexData; 101 } 102 103 /** 104 * Return the conversion of {@link SearchIndexableRaw} to {@link IndexData}. 105 * The fields of {@link SearchIndexableRaw} are a subset of {@link IndexData}, 106 * and there is some data sanitization in the conversion. 107 */ 108 @Nullable 109 private IndexData.Builder convertRaw(SearchIndexableRaw raw, Set<String> nonIndexableKeys) { 110 // A row is enabled if it does not show up as an nonIndexableKey 111 boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key)); 112 113 IndexData.Builder builder = new IndexData.Builder(); 114 builder.setTitle(raw.title) 115 .setSummaryOn(raw.summaryOn) 116 .setEntries(raw.entries) 117 .setKeywords(raw.keywords) 118 .setClassName(raw.className) 119 .setScreenTitle(raw.screenTitle) 120 .setIconResId(raw.iconResId) 121 .setIntentAction(raw.intentAction) 122 .setIntentTargetPackage(raw.intentTargetPackage) 123 .setIntentTargetClass(raw.intentTargetClass) 124 .setEnabled(enabled) 125 .setKey(raw.key) 126 .setUserId(raw.userId); 127 128 return builder; 129 } 130 131 /** 132 * Return the conversion of the {@link SearchIndexableResource} to {@link IndexData}. 133 * Each of the elements in the xml layout attribute of {@param sir} is a candidate to be 134 * converted (including the header element). 135 * 136 * TODO (b/33577327) simplify this method. 137 */ 138 private List<IndexData> convertResource(SearchIndexableResource sir, 139 Set<String> nonIndexableKeys) { 140 final Context context = sir.context; 141 XmlResourceParser parser = null; 142 143 List<IndexData> resourceIndexData = new ArrayList<>(); 144 try { 145 parser = context.getResources().getXml(sir.xmlResId); 146 147 int type; 148 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 149 && type != XmlPullParser.START_TAG) { 150 // Parse next until start tag is found 151 } 152 153 String nodeName = parser.getName(); 154 if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { 155 throw new RuntimeException( 156 "XML document must start with <PreferenceScreen> tag; found" 157 + nodeName + " at " + parser.getPositionDescription()); 158 } 159 160 final int outerDepth = parser.getDepth(); 161 final AttributeSet attrs = Xml.asAttributeSet(parser); 162 163 final String screenTitle = PreferenceXmlParserUtils.getDataTitle(context, attrs); 164 String key = PreferenceXmlParserUtils.getDataKey(context, attrs); 165 166 String title; 167 String headerTitle; 168 String summary; 169 String headerSummary; 170 String keywords; 171 String headerKeywords; 172 String childFragment; 173 @DrawableRes int iconResId; 174 ResultPayload payload; 175 boolean enabled; 176 final String fragmentName = sir.className; 177 final String intentAction = sir.intentAction; 178 final String intentTargetPackage = sir.intentTargetPackage; 179 final String intentTargetClass = sir.intentTargetClass; 180 181 Map<String, ResultPayload> controllerUriMap = new HashMap<>(); 182 183 if (fragmentName != null) { 184 controllerUriMap = DatabaseIndexingUtils 185 .getPayloadKeyMap(fragmentName, context); 186 } 187 188 headerTitle = PreferenceXmlParserUtils.getDataTitle(context, attrs); 189 headerSummary = PreferenceXmlParserUtils.getDataSummary(context, attrs); 190 headerKeywords = PreferenceXmlParserUtils.getDataKeywords(context, attrs); 191 enabled = !nonIndexableKeys.contains(key); 192 193 // TODO: Set payload type for header results 194 IndexData.Builder headerBuilder = new IndexData.Builder(); 195 headerBuilder.setTitle(headerTitle) 196 .setSummaryOn(headerSummary) 197 .setKeywords(headerKeywords) 198 .setClassName(fragmentName) 199 .setScreenTitle(screenTitle) 200 .setIntentAction(intentAction) 201 .setIntentTargetPackage(intentTargetPackage) 202 .setIntentTargetClass(intentTargetClass) 203 .setEnabled(enabled) 204 .setKey(key) 205 .setUserId(-1 /* default user id */); 206 207 // Flag for XML headers which a child element's title. 208 boolean isHeaderUnique = true; 209 IndexData.Builder builder; 210 211 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 212 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 213 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 214 continue; 215 } 216 217 nodeName = parser.getName(); 218 219 title = PreferenceXmlParserUtils.getDataTitle(context, attrs); 220 key = PreferenceXmlParserUtils.getDataKey(context, attrs); 221 enabled = !nonIndexableKeys.contains(key); 222 keywords = PreferenceXmlParserUtils.getDataKeywords(context, attrs); 223 iconResId = PreferenceXmlParserUtils.getDataIcon(context, attrs); 224 225 if (isHeaderUnique && TextUtils.equals(headerTitle, title)) { 226 isHeaderUnique = false; 227 } 228 229 builder = new IndexData.Builder(); 230 builder.setTitle(title) 231 .setKeywords(keywords) 232 .setClassName(fragmentName) 233 .setScreenTitle(screenTitle) 234 .setIconResId(iconResId) 235 .setIntentAction(intentAction) 236 .setIntentTargetPackage(intentTargetPackage) 237 .setIntentTargetClass(intentTargetClass) 238 .setEnabled(enabled) 239 .setKey(key) 240 .setUserId(-1 /* default user id */); 241 242 if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { 243 summary = PreferenceXmlParserUtils.getDataSummary(context, attrs); 244 245 String entries = null; 246 247 if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { 248 entries = PreferenceXmlParserUtils.getDataEntries(context, attrs); 249 } 250 251 // TODO (b/62254931) index primitives instead of payload 252 payload = controllerUriMap.get(key); 253 childFragment = PreferenceXmlParserUtils.getDataChildFragment(context, attrs); 254 255 builder.setSummaryOn(summary) 256 .setEntries(entries) 257 .setChildClassName(childFragment) 258 .setPayload(payload); 259 260 resourceIndexData.add(builder.build(mContext)); 261 } else { 262 // TODO (b/33577327) We removed summary off here. We should check if we can 263 // merge this 'else' section with the one above. Put a break point to 264 // investigate. 265 String summaryOn = PreferenceXmlParserUtils.getDataSummaryOn(context, attrs); 266 String summaryOff = PreferenceXmlParserUtils.getDataSummaryOff(context, attrs); 267 268 if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) { 269 summaryOn = PreferenceXmlParserUtils.getDataSummary(context, attrs); 270 } 271 272 builder.setSummaryOn(summaryOn); 273 274 resourceIndexData.add(builder.build(mContext)); 275 } 276 } 277 278 // The xml header's title does not match the title of one of the child settings. 279 if (isHeaderUnique) { 280 resourceIndexData.add(headerBuilder.build(mContext)); 281 } 282 } catch (XmlPullParserException e) { 283 Log.w(LOG_TAG, "XML Error parsing PreferenceScreen: ", e); 284 } catch (IOException e) { 285 Log.w(LOG_TAG, "IO Error parsing PreferenceScreen: ", e); 286 } catch (Resources.NotFoundException e) { 287 Log.w(LOG_TAG, "Resoucre not found error parsing PreferenceScreen: ", e); 288 } finally { 289 if (parser != null) parser.close(); 290 } 291 return resourceIndexData; 292 } 293 294 private Set<String> getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys, 295 String packageName) { 296 return nonIndexableKeys.containsKey(packageName) 297 ? nonIndexableKeys.get(packageName) 298 : new HashSet<>(); 299 } 300} 301