• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.content.res.XmlResourceParser;
24 import android.provider.SearchIndexableData;
25 import android.provider.SearchIndexableResource;
26 import android.support.annotation.DrawableRes;
27 import android.text.TextUtils;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.util.Xml;
31 
32 import com.android.settings.search.DatabaseIndexingUtils;
33 import com.android.settings.core.PreferenceXmlParserUtils;
34 import com.android.settings.search.ResultPayload;
35 import com.android.settings.search.SearchIndexableRaw;
36 
37 import org.xmlpull.v1.XmlPullParser;
38 import org.xmlpull.v1.XmlPullParserException;
39 
40 import java.io.IOException;
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Set;
47 
48 /**
49  * Helper class to convert {@link PreIndexData} to {@link IndexData}.
50  */
51 public 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 
IndexDataConverter(Context context)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      */
convertPreIndexDataToIndexData(PreIndexData preIndexData)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
convertRaw(SearchIndexableRaw raw, Set<String> nonIndexableKeys)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      */
convertResource(SearchIndexableResource sir, Set<String> nonIndexableKeys)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 
getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys, String packageName)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