• 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.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