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